From 6d250b0ad75cddf5cebea45b3ea058d4e5c47877 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Fri, 28 Apr 2023 10:26:40 -0500 Subject: [PATCH 1/6] feat(api/types): add support for schedules --- api/types/schedule.go | 274 +++++++++++++++++++++++++++++++++++++ api/types/schedule_test.go | 197 ++++++++++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 api/types/schedule.go create mode 100644 api/types/schedule_test.go diff --git a/api/types/schedule.go b/api/types/schedule.go new file mode 100644 index 000000000..73a087348 --- /dev/null +++ b/api/types/schedule.go @@ -0,0 +1,274 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package types + +import ( + "fmt" + + "github.com/go-vela/types/library" +) + +// Schedule is the API representation of a schedule for a repo. +// +// swagger:model Schedule +type Schedule struct { + ID *int64 `json:"id,omitempty"` + Active *bool `json:"active,omitempty"` + Name *string `json:"name,omitempty"` + Entry *string `json:"entry,omitempty"` + CreatedAt *int64 `json:"created_at,omitempty"` + CreatedBy *string `json:"created_by,omitempty"` + UpdatedAt *int64 `json:"updated_at,omitempty"` + UpdatedBy *string `json:"updated_by,omitempty"` + ScheduledAt *int64 `json:"scheduled_at,omitempty"` + Repo *library.Repo `json:"repo,omitempty"` +} + +// GetID returns the ID field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetID() int64 { + // return zero value if Schedule type or ID field is nil + if s == nil || s.ID == nil { + return 0 + } + + return *s.ID +} + +// GetActive returns the Active field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetActive() bool { + // return zero value if Schedule type or Active field is nil + if s == nil || s.Active == nil { + return false + } + + return *s.Active +} + +// GetName returns the Name field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetName() string { + // return zero value if Schedule type or Name field is nil + if s == nil || s.Name == nil { + return "" + } + + return *s.Name +} + +// GetEntry returns the Entry field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetEntry() string { + // return zero value if Schedule type or Entry field is nil + if s == nil || s.Entry == nil { + return "" + } + + return *s.Entry +} + +// GetCreatedAt returns the CreatedAt field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetCreatedAt() int64 { + // return zero value if Schedule type or CreatedAt field is nil + if s == nil || s.CreatedAt == nil { + return 0 + } + + return *s.CreatedAt +} + +// GetCreatedBy returns the CreatedBy field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetCreatedBy() string { + // return zero value if Schedule type or CreatedBy field is nil + if s == nil || s.CreatedBy == nil { + return "" + } + + return *s.CreatedBy +} + +// GetUpdatedAt returns the UpdatedAt field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetUpdatedAt() int64 { + // return zero value if Schedule type or UpdatedAt field is nil + if s == nil || s.UpdatedAt == nil { + return 0 + } + + return *s.UpdatedAt +} + +// GetUpdatedBy returns the UpdatedBy field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetUpdatedBy() string { + // return zero value if Schedule type or UpdatedBy field is nil + if s == nil || s.UpdatedBy == nil { + return "" + } + + return *s.UpdatedBy +} + +// GetScheduledAt returns the ScheduledAt field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetScheduledAt() int64 { + // return zero value if Schedule type or ScheduledAt field is nil + if s == nil || s.ScheduledAt == nil { + return 0 + } + + return *s.ScheduledAt +} + +// GetRepo returns the Repo field from the provided Schedule. If the object is nil, +// or the field within the object is nil, it returns the zero value instead. +func (s *Schedule) GetRepo() *library.Repo { + // return zero value if Schedule type or Repo field is nil + if s == nil || s.Repo == nil { + return new(library.Repo) + } + + return s.Repo +} + +// SetID sets the ID field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetID(id int64) { + // return if Schedule type is nil + if s == nil { + return + } + + s.ID = &id +} + +// SetActive sets the Active field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetActive(active bool) { + // return if Schedule type is nil + if s == nil { + return + } + + s.Active = &active +} + +// SetName sets the Name field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetName(name string) { + // return if Schedule type is nil + if s == nil { + return + } + + s.Name = &name +} + +// SetEntry sets the Entry field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetEntry(entry string) { + // return if Schedule type is nil + if s == nil { + return + } + + s.Entry = &entry +} + +// SetCreatedAt sets the CreatedAt field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetCreatedAt(createdAt int64) { + // return if Schedule type is nil + if s == nil { + return + } + + s.CreatedAt = &createdAt +} + +// SetCreatedBy sets the CreatedBy field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetCreatedBy(createdBy string) { + // return if Schedule type is nil + if s == nil { + return + } + + s.CreatedBy = &createdBy +} + +// SetUpdatedAt sets the UpdatedAt field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetUpdatedAt(updatedAt int64) { + // return if Schedule type is nil + if s == nil { + return + } + + s.UpdatedAt = &updatedAt +} + +// SetUpdatedBy sets the UpdatedBy field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetUpdatedBy(updatedBy string) { + // return if Schedule type is nil + if s == nil { + return + } + + s.UpdatedBy = &updatedBy +} + +// SetScheduledAt sets the ScheduledAt field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetScheduledAt(scheduledAt int64) { + // return if Schedule type is nil + if s == nil { + return + } + + s.ScheduledAt = &scheduledAt +} + +// SetRepo sets the Repo field in the provided Schedule. If the object is nil, +// it will set nothing and immediately return making this a no-op. +func (s *Schedule) SetRepo(repo *library.Repo) { + // return if Schedule type is nil + if s == nil { + return + } + + s.Repo = repo +} + +// String implements the Stringer interface for the Schedule type. +func (s *Schedule) String() string { + return fmt.Sprintf(`{ + Active: %t, + CreatedAt: %d, + CreatedBy: %s, + Entry: %s, + ID: %d, + Name: %s, + ScheduledAt: %d, + UpdatedAt: %d, + UpdatedBy: %s, + Repo: %v, +}`, + s.GetActive(), + s.GetCreatedAt(), + s.GetCreatedBy(), + s.GetEntry(), + s.GetID(), + s.GetName(), + s.GetScheduledAt(), + s.GetUpdatedAt(), + s.GetUpdatedBy(), + s.GetRepo(), + ) +} diff --git a/api/types/schedule_test.go b/api/types/schedule_test.go new file mode 100644 index 000000000..afc5d2e4b --- /dev/null +++ b/api/types/schedule_test.go @@ -0,0 +1,197 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package types + +import ( + "fmt" + "reflect" + "strings" + "testing" + "time" + + "github.com/go-vela/types/library" +) + +func TestTypes_Schedule_Getters(t *testing.T) { + tests := []struct { + name string + schedule *Schedule + want *Schedule + }{ + { + name: "schedule with fields", + schedule: testSchedule(), + want: testSchedule(), + }, + { + name: "schedule with empty fields", + schedule: new(Schedule), + want: new(Schedule), + }, + { + name: "empty schedule", + schedule: nil, + want: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.schedule.GetID() != test.want.GetID() { + t.Errorf("GetID is %v, want %v", test.schedule.GetID(), test.want.GetID()) + } + if test.schedule.GetActive() != test.want.GetActive() { + t.Errorf("GetActive is %v, want %v", test.schedule.GetActive(), test.want.GetActive()) + } + if test.schedule.GetName() != test.want.GetName() { + t.Errorf("GetName is %v, want %v", test.schedule.GetName(), test.want.GetName()) + } + if test.schedule.GetEntry() != test.want.GetEntry() { + t.Errorf("GetEntry is %v, want %v", test.schedule.GetEntry(), test.want.GetEntry()) + } + if test.schedule.GetCreatedAt() != test.want.GetCreatedAt() { + t.Errorf("GetCreatedAt is %v, want %v", test.schedule.GetCreatedAt(), test.want.GetCreatedAt()) + } + if test.schedule.GetCreatedBy() != test.want.GetCreatedBy() { + t.Errorf("GetCreatedBy is %v, want %v", test.schedule.GetCreatedBy(), test.want.GetCreatedBy()) + } + if test.schedule.GetUpdatedAt() != test.want.GetUpdatedAt() { + t.Errorf("GetUpdatedAt is %v, want %v", test.schedule.GetUpdatedAt(), test.want.GetUpdatedAt()) + } + if test.schedule.GetUpdatedBy() != test.want.GetUpdatedBy() { + t.Errorf("GetUpdatedBy is %v, want %v", test.schedule.GetUpdatedBy(), test.want.GetUpdatedBy()) + } + if test.schedule.GetScheduledAt() != test.want.GetScheduledAt() { + t.Errorf("GetScheduledAt is %v, want %v", test.schedule.GetScheduledAt(), test.want.GetScheduledAt()) + } + if !reflect.DeepEqual(test.schedule.GetRepo(), test.want.GetRepo()) { + t.Errorf("GetRepo is %v, want %v", test.schedule.GetRepo(), test.want.GetRepo()) + } + }) + } +} + +func TestTypes_Schedule_Setters(t *testing.T) { + tests := []struct { + name string + schedule *Schedule + want *Schedule + }{ + { + name: "schedule with fields", + schedule: testSchedule(), + want: testSchedule(), + }, + { + name: "schedule with empty fields", + schedule: new(Schedule), + want: new(Schedule), + }, + { + name: "empty schedule", + schedule: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.schedule.SetID(test.want.GetID()) + if test.schedule.GetID() != test.want.GetID() { + t.Errorf("SetID is %v, want %v", test.schedule.GetID(), test.want.GetID()) + } + test.schedule.SetActive(test.want.GetActive()) + if test.schedule.GetActive() != test.want.GetActive() { + t.Errorf("SetActive is %v, want %v", test.schedule.GetActive(), test.want.GetActive()) + } + test.schedule.SetName(test.want.GetName()) + if test.schedule.GetName() != test.want.GetName() { + t.Errorf("SetName is %v, want %v", test.schedule.GetName(), test.want.GetName()) + } + test.schedule.SetEntry(test.want.GetEntry()) + if test.schedule.GetEntry() != test.want.GetEntry() { + t.Errorf("SetEntry is %v, want %v", test.schedule.GetEntry(), test.want.GetEntry()) + } + test.schedule.SetCreatedAt(test.want.GetCreatedAt()) + if test.schedule.GetCreatedAt() != test.want.GetCreatedAt() { + t.Errorf("SetCreatedAt is %v, want %v", test.schedule.GetCreatedAt(), test.want.GetCreatedAt()) + } + test.schedule.SetCreatedBy(test.want.GetCreatedBy()) + if test.schedule.GetCreatedBy() != test.want.GetCreatedBy() { + t.Errorf("SetCreatedBy is %v, want %v", test.schedule.GetCreatedBy(), test.want.GetCreatedBy()) + } + test.schedule.SetUpdatedAt(test.want.GetUpdatedAt()) + if test.schedule.GetUpdatedAt() != test.want.GetUpdatedAt() { + t.Errorf("SetUpdatedAt is %v, want %v", test.schedule.GetUpdatedAt(), test.want.GetUpdatedAt()) + } + test.schedule.SetUpdatedBy(test.want.GetUpdatedBy()) + if test.schedule.GetUpdatedBy() != test.want.GetUpdatedBy() { + t.Errorf("SetUpdatedBy is %v, want %v", test.schedule.GetUpdatedBy(), test.want.GetUpdatedBy()) + } + test.schedule.SetScheduledAt(test.want.GetScheduledAt()) + if test.schedule.GetScheduledAt() != test.want.GetScheduledAt() { + t.Errorf("SetScheduledAt is %v, want %v", test.schedule.GetScheduledAt(), test.want.GetScheduledAt()) + } + test.schedule.SetRepo(test.want.GetRepo()) + if !reflect.DeepEqual(test.schedule.GetRepo(), test.want.GetRepo()) { + t.Errorf("SetRepo is %v, want %v", test.schedule.GetRepo(), test.want.GetRepo()) + } + }) + } +} + +func TestLibrary_Schedule_String(t *testing.T) { + s := testSchedule() + + want := fmt.Sprintf(`{ + Active: %t, + CreatedAt: %d, + CreatedBy: %s, + Entry: %s, + ID: %d, + Name: %s, + ScheduledAt: %d, + UpdatedAt: %d, + UpdatedBy: %s, + Repo: %v, +}`, + s.GetActive(), + s.GetCreatedAt(), + s.GetCreatedBy(), + s.GetEntry(), + s.GetID(), + s.GetName(), + s.GetScheduledAt(), + s.GetUpdatedAt(), + s.GetUpdatedBy(), + s.GetRepo(), + ) + + got := s.String() + if !strings.EqualFold(got, want) { + t.Errorf("String is %v, want %v", got, want) + } +} + +// testSchedule is a test helper function to create a Schedule type with all fields set to a fake value. +func testSchedule() *Schedule { + r := new(library.Repo) + r.SetID(1) + + s := new(Schedule) + s.SetID(1) + s.SetActive(true) + s.SetName("nightly") + s.SetEntry("0 0 * * *") + s.SetCreatedAt(time.Now().UTC().Unix()) + s.SetCreatedBy("user1") + s.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + s.SetUpdatedBy("user2") + s.SetScheduledAt(time.Now().Add(time.Hour * 2).UTC().Unix()) + s.SetRepo(r) + + return s +} From 661dca883f6b00694e8b26f5062427e04ee22156 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Fri, 28 Apr 2023 10:27:01 -0500 Subject: [PATCH 2/6] feat(database/types): add support for schedules --- database/types/sanitize.go | 44 ++++++++ database/types/sanitize_test.go | 85 +++++++++++++++ database/types/schedule.go | 132 +++++++++++++++++++++++ database/types/schedule_test.go | 178 ++++++++++++++++++++++++++++++++ 4 files changed, 439 insertions(+) create mode 100644 database/types/sanitize.go create mode 100644 database/types/sanitize_test.go create mode 100644 database/types/schedule.go create mode 100644 database/types/schedule_test.go diff --git a/database/types/sanitize.go b/database/types/sanitize.go new file mode 100644 index 000000000..91d7af137 --- /dev/null +++ b/database/types/sanitize.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package types + +import ( + "html" + "net/url" + "strings" + + "github.com/microcosm-cc/bluemonday" +) + +// sanitize is a helper function to verify the provided input +// field does not contain HTML content. If the input field +// does contain HTML, then the function will sanitize and +// potentially remove the HTML if deemed malicious. +func sanitize(field string) string { + // create new HTML input microcosm-cc/bluemonday policy + p := bluemonday.StrictPolicy() + + // create a URL query unescaped string from the field + queryUnescaped, err := url.QueryUnescape(field) + if err != nil { + // overwrite URL query unescaped string with field + queryUnescaped = field + } + + // create an HTML escaped string from the field + htmlEscaped := html.EscapeString(queryUnescaped) + + // create a microcosm-cc/bluemonday escaped string from the field + bluemondayEscaped := p.Sanitize(queryUnescaped) + + // check if the field contains html + if !strings.EqualFold(htmlEscaped, bluemondayEscaped) { + // create new HTML input microcosm-cc/bluemonday policy + return bluemondayEscaped + } + + // return the unmodified field + return field +} diff --git a/database/types/sanitize_test.go b/database/types/sanitize_test.go new file mode 100644 index 000000000..4a39ec51d --- /dev/null +++ b/database/types/sanitize_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package types + +import ( + "testing" +) + +func TestTypes_Sanitize(t *testing.T) { + // setup tests + tests := []struct { + name string + value string + want string + }{ + { + name: "percent", + value: `%`, + want: `%`, + }, + { + name: "quoted", + value: `"hello"`, + want: `"hello"`, + }, + { + name: "email", + value: `OctoKitty@github.com`, + want: `OctoKitty@github.com`, + }, + { + name: "url", + value: `https://github.com/go-vela`, + want: `https://github.com/go-vela`, + }, + { + name: "encoded", + value: `+ added foo %25 + updated bar %22 +`, + want: `+ added foo %25 + updated bar %22 +`, + }, + { + name: "html with headers", + value: `Merge pull request #1 from me/patch-1\n\n

hello

is now

hello

`, + want: `Merge pull request #1 from me/patch-1\n\nhello is now hello`, + }, + { + name: "html with email", + value: `Co-authored-by: OctoKitty `, + want: `Co-authored-by: OctoKitty `, + }, + { + name: "html with href", + value: `Google`, + want: `Google`, + }, + { + name: "local cross-site script", + value: ``, + want: ``, + }, + { + name: "remote cross-site script", + value: ``, + want: ``, + }, + { + name: "embedded cross-site script", + value: `%3cDIV%20STYLE%3d%22width%3a%20expression(alert('XSS'))%3b%22%3e`, + want: ``, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := sanitize(test.value) + + if got != test.want { + t.Errorf("sanitize is %v, want %v", got, test.want) + } + }) + } +} diff --git a/database/types/schedule.go b/database/types/schedule.go new file mode 100644 index 000000000..40ac818e6 --- /dev/null +++ b/database/types/schedule.go @@ -0,0 +1,132 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package types + +import ( + "database/sql" + "errors" + + "github.com/go-vela/server/api/types" + "github.com/go-vela/types/library" +) + +var ( + // ErrEmptyScheduleEntry defines the error type when a + // Schedule type has an empty Entry field provided. + ErrEmptyScheduleEntry = errors.New("empty schedule entry provided") + + // ErrEmptyScheduleName defines the error type when a + // Schedule type has an empty Name field provided. + ErrEmptyScheduleName = errors.New("empty schedule name provided") + + // ErrEmptyScheduleRepoID defines the error type when a + // Schedule type has an empty RepoID field provided. + ErrEmptyScheduleRepoID = errors.New("empty schedule repo_id provided") +) + +// Schedule is the database representation of a schedule for a repo. +type Schedule struct { + ID sql.NullInt64 `sql:"id"` + RepoID sql.NullInt64 `sql:"repo_id"` + Active sql.NullBool `sql:"active"` + Name sql.NullString `sql:"name"` + Entry sql.NullString `sql:"entry"` + CreatedAt sql.NullInt64 `sql:"created_at"` + CreatedBy sql.NullString `sql:"created_by"` + UpdatedAt sql.NullInt64 `sql:"updated_at"` + UpdatedBy sql.NullString `sql:"updated_by"` + ScheduledAt sql.NullInt64 `sql:"scheduled_at"` +} + +// ScheduleFromAPI converts the Schedule type to an API schedule type. +func ScheduleFromAPI(s *types.Schedule) *Schedule { + schedule := &Schedule{ + ID: sql.NullInt64{Int64: s.GetID(), Valid: true}, + RepoID: sql.NullInt64{Int64: s.GetRepo().GetID(), Valid: true}, + Active: sql.NullBool{Bool: s.GetActive(), Valid: true}, + Name: sql.NullString{String: s.GetName(), Valid: true}, + Entry: sql.NullString{String: s.GetEntry(), Valid: true}, + CreatedAt: sql.NullInt64{Int64: s.GetCreatedAt(), Valid: true}, + CreatedBy: sql.NullString{String: s.GetCreatedBy(), Valid: true}, + UpdatedAt: sql.NullInt64{Int64: s.GetUpdatedAt(), Valid: true}, + UpdatedBy: sql.NullString{String: s.GetUpdatedBy(), Valid: true}, + ScheduledAt: sql.NullInt64{Int64: s.GetScheduledAt(), Valid: true}, + } + + return schedule.Nullify() +} + +// Nullify ensures the valid flag for the sql.Null types are properly set. +// +// When a field within the Schedule type is the zero value for the field, the +// valid flag is set to false causing it to be NULL in the database. +func (s *Schedule) Nullify() *Schedule { + if s == nil { + return nil + } + + // check if the ID field should be valid + s.ID.Valid = s.ID.Int64 != 0 + // check if the RepoID field should be valid + s.RepoID.Valid = s.RepoID.Int64 != 0 + // check if the ID field should be valid + s.Active.Valid = s.RepoID.Int64 != 0 + // check if the Name field should be valid + s.Name.Valid = len(s.Name.String) != 0 + // check if the Entry field should be valid + s.Entry.Valid = len(s.Entry.String) != 0 + // check if the CreatedAt field should be valid + s.CreatedAt.Valid = s.CreatedAt.Int64 != 0 + // check if the CreatedBy field should be valid + s.CreatedBy.Valid = len(s.CreatedBy.String) != 0 + // check if the UpdatedAt field should be valid + s.UpdatedAt.Valid = s.UpdatedAt.Int64 != 0 + // check if the UpdatedBy field should be valid + s.UpdatedBy.Valid = len(s.UpdatedBy.String) != 0 + // check if the ScheduledAt field should be valid + s.ScheduledAt.Valid = s.ScheduledAt.Int64 != 0 + + return s +} + +// ToAPI converts the Schedule type to an API Schedule type. +func (s *Schedule) ToAPI(r *library.Repo) *types.Schedule { + return &types.Schedule{ + ID: &s.ID.Int64, + Active: &s.Active.Bool, + Name: &s.Name.String, + Entry: &s.Entry.String, + CreatedAt: &s.CreatedAt.Int64, + CreatedBy: &s.CreatedBy.String, + UpdatedAt: &s.UpdatedAt.Int64, + UpdatedBy: &s.UpdatedBy.String, + ScheduledAt: &s.ScheduledAt.Int64, + Repo: r, + } +} + +// Validate verifies the necessary fields for the Schedule type are populated correctly. +func (s *Schedule) Validate() error { + // verify the RepoID field is populated + if s.RepoID.Int64 <= 0 { + return ErrEmptyScheduleRepoID + } + + // verify the Name field is populated + if len(s.Name.String) <= 0 { + return ErrEmptyScheduleName + } + + // verify the Entry field is populated + if len(s.Entry.String) <= 0 { + return ErrEmptyScheduleEntry + } + + // ensure that all Schedule string fields that can be returned as JSON are sanitized to avoid unsafe HTML content + s.Name = sql.NullString{String: sanitize(s.Name.String), Valid: s.Name.Valid} + s.Entry = sql.NullString{String: sanitize(s.Entry.String), Valid: s.Entry.Valid} + + return nil +} diff --git a/database/types/schedule_test.go b/database/types/schedule_test.go new file mode 100644 index 000000000..0a96bd52e --- /dev/null +++ b/database/types/schedule_test.go @@ -0,0 +1,178 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package types + +import ( + "database/sql" + "reflect" + "testing" + "time" + + "github.com/go-vela/server/api/types" + + "github.com/go-vela/types/library" +) + +func TestTypes_ScheduleFromAPI(t *testing.T) { + r := new(library.Repo) + r.SetID(1) + + s := new(types.Schedule) + s.SetID(1) + s.SetActive(true) + s.SetName("nightly") + s.SetEntry("0 0 * * *") + s.SetCreatedAt(time.Now().UTC().Unix()) + s.SetCreatedBy("user1") + s.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + s.SetUpdatedBy("user2") + s.SetScheduledAt(time.Now().Add(time.Hour * 2).UTC().Unix()) + s.SetRepo(r) + + want := testSchedule() + + got := ScheduleFromAPI(s) + if !reflect.DeepEqual(got, want) { + t.Errorf("ScheduleFromAPI is %v, want %v", got, want) + } +} + +func TestTypes_Schedule_Nullify(t *testing.T) { + tests := []struct { + name string + schedule *Schedule + want *Schedule + }{ + { + name: "schedule with fields", + schedule: testSchedule(), + want: testSchedule(), + }, + { + name: "schedule with empty fields", + schedule: new(Schedule), + want: &Schedule{ + ID: sql.NullInt64{Int64: 0, Valid: false}, + RepoID: sql.NullInt64{Int64: 0, Valid: false}, + Active: sql.NullBool{Bool: false, Valid: false}, + Name: sql.NullString{String: "", Valid: false}, + Entry: sql.NullString{String: "", Valid: false}, + CreatedAt: sql.NullInt64{Int64: 0, Valid: false}, + CreatedBy: sql.NullString{String: "", Valid: false}, + UpdatedAt: sql.NullInt64{Int64: 0, Valid: false}, + UpdatedBy: sql.NullString{String: "", Valid: false}, + ScheduledAt: sql.NullInt64{Int64: 0, Valid: false}, + }, + }, + { + name: "empty schedule", + schedule: nil, + want: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.schedule.Nullify() + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Nullify is %v, want %v", got, test.want) + } + }) + } +} + +func TestTypes_Schedule_ToAPI(t *testing.T) { + r := new(library.Repo) + r.SetID(1) + + want := new(types.Schedule) + want.SetID(1) + want.SetActive(true) + want.SetName("nightly") + want.SetEntry("0 0 * * *") + want.SetCreatedAt(time.Now().UTC().Unix()) + want.SetCreatedBy("user1") + want.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + want.SetUpdatedBy("user2") + want.SetScheduledAt(time.Now().Add(time.Hour * 2).UTC().Unix()) + want.SetRepo(r) + + got := testSchedule().ToAPI(r) + if !reflect.DeepEqual(got, want) { + t.Errorf("ToAPI is %v, want %v", got, want) + } +} + +func TestTypes_Schedule_Validate(t *testing.T) { + tests := []struct { + name string + failure bool + schedule *Schedule + }{ + { + name: "schedule with valid fields", + failure: false, + schedule: testSchedule(), + }, + { + name: "schedule with missing entry", + failure: true, + schedule: &Schedule{ + ID: sql.NullInt64{Int64: 1, Valid: true}, + RepoID: sql.NullInt64{Int64: 1, Valid: true}, + Name: sql.NullString{String: "nightly", Valid: false}, + }, + }, + { + name: "schedule with missing name", + failure: true, + schedule: &Schedule{ + ID: sql.NullInt64{Int64: 1, Valid: true}, + RepoID: sql.NullInt64{Int64: 1, Valid: true}, + Entry: sql.NullString{String: "0 0 * * *", Valid: false}, + }, + }, + { + name: "schedule with missing repo_id", + failure: true, + schedule: &Schedule{ + ID: sql.NullInt64{Int64: 1, Valid: true}, + Name: sql.NullString{String: "nightly", Valid: false}, + Entry: sql.NullString{String: "0 0 * * *", Valid: false}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.schedule.Validate() + if test.failure { + if err == nil { + t.Errorf("Validate should have returned err") + } + return + } + if err != nil { + t.Errorf("Validate returned err: %v", err) + } + }) + } +} + +// testSchedule is a test helper function to create a Schedule type with all fields set to a fake value. +func testSchedule() *Schedule { + return &Schedule{ + ID: sql.NullInt64{Int64: 1, Valid: true}, + RepoID: sql.NullInt64{Int64: 1, Valid: true}, + Active: sql.NullBool{Bool: true, Valid: true}, + Name: sql.NullString{String: "nightly", Valid: true}, + Entry: sql.NullString{String: "0 0 * * *", Valid: true}, + CreatedAt: sql.NullInt64{Int64: time.Now().UTC().Unix(), Valid: true}, + CreatedBy: sql.NullString{String: "user1", Valid: true}, + UpdatedAt: sql.NullInt64{Int64: time.Now().Add(time.Hour * 1).UTC().Unix(), Valid: true}, + UpdatedBy: sql.NullString{String: "user2", Valid: true}, + ScheduledAt: sql.NullInt64{Int64: time.Now().Add(time.Hour * 2).UTC().Unix(), Valid: true}, + } +} From 737e5f8ccc49ee1c903becdfd3b2ef90a828bfe4 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Fri, 28 Apr 2023 10:31:00 -0500 Subject: [PATCH 3/6] chore: update go dependencies --- go.mod | 3 ++- go.sum | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index fd31f02c0..e12fc7532 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.2 github.com/hashicorp/vault/api v1.9.1 github.com/joho/godotenv v1.5.1 + github.com/microcosm-cc/bluemonday v1.0.23 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.15.0 github.com/redis/go-redis/v9 v9.0.3 @@ -91,7 +92,6 @@ require ( github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/microcosm-cc/bluemonday v1.0.23 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -102,6 +102,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect diff --git a/go.sum b/go.sum index 60fa83003..7ba75f88d 100644 --- a/go.sum +++ b/go.sum @@ -347,7 +347,8 @@ github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= From 328afc06594a0bfca6034864024c39cd6eee4836 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Mon, 1 May 2023 15:48:32 -0500 Subject: [PATCH 4/6] fix: parse entry for schedules --- database/types/schedule.go | 21 +++++++++++++++------ database/types/schedule_test.go | 11 ++++++++++- go.mod | 1 + go.sum | 2 ++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/database/types/schedule.go b/database/types/schedule.go index 40ac818e6..9a2e58ecc 100644 --- a/database/types/schedule.go +++ b/database/types/schedule.go @@ -10,20 +10,24 @@ import ( "github.com/go-vela/server/api/types" "github.com/go-vela/types/library" + "github.com/robfig/cron/v3" ) var ( - // ErrEmptyScheduleEntry defines the error type when a - // Schedule type has an empty Entry field provided. + // ErrEmptyScheduleEntry defines the error type when a Schedule type has an empty Entry field provided. ErrEmptyScheduleEntry = errors.New("empty schedule entry provided") - // ErrEmptyScheduleName defines the error type when a - // Schedule type has an empty Name field provided. + // ErrEmptyScheduleName defines the error type when a Schedule type has an empty Name field provided. ErrEmptyScheduleName = errors.New("empty schedule name provided") - // ErrEmptyScheduleRepoID defines the error type when a - // Schedule type has an empty RepoID field provided. + // ErrEmptyScheduleRepoID defines the error type when a Schedule type has an empty RepoID field provided. ErrEmptyScheduleRepoID = errors.New("empty schedule repo_id provided") + + // ErrInvalidScheduleEntry defines the error type when a Schedule type has an invalid Entry field provided. + ErrInvalidScheduleEntry = errors.New("invalid schedule entry provided") + + // scheduleParser defines the parser used for validating the Entry field for the Schedule type. + scheduleParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) ) // Schedule is the database representation of a schedule for a repo. @@ -124,6 +128,11 @@ func (s *Schedule) Validate() error { return ErrEmptyScheduleEntry } + _, err := scheduleParser.Parse(s.Entry.String) + if err != nil { + return ErrInvalidScheduleEntry + } + // ensure that all Schedule string fields that can be returned as JSON are sanitized to avoid unsafe HTML content s.Name = sql.NullString{String: sanitize(s.Name.String), Valid: s.Name.Valid} s.Entry = sql.NullString{String: sanitize(s.Entry.String), Valid: s.Entry.Valid} diff --git a/database/types/schedule_test.go b/database/types/schedule_test.go index 0a96bd52e..c7ead8eee 100644 --- a/database/types/schedule_test.go +++ b/database/types/schedule_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/go-vela/server/api/types" - "github.com/go-vela/types/library" ) @@ -116,6 +115,16 @@ func TestTypes_Schedule_Validate(t *testing.T) { failure: false, schedule: testSchedule(), }, + { + name: "schedule with invalid entry", + failure: true, + schedule: &Schedule{ + ID: sql.NullInt64{Int64: 1, Valid: true}, + RepoID: sql.NullInt64{Int64: 1, Valid: true}, + Name: sql.NullString{String: "invalid", Valid: false}, + Entry: sql.NullString{String: "!@#$%^&*()", Valid: false}, + }, + }, { name: "schedule with missing entry", failure: true, diff --git a/go.mod b/go.mod index e12fc7532..c785314ab 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.15.0 github.com/redis/go-redis/v9 v9.0.3 + github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.0 github.com/spf13/afero v1.9.5 github.com/urfave/cli/v2 v2.25.1 diff --git a/go.sum b/go.sum index 7ba75f88d..14579af4c 100644 --- a/go.sum +++ b/go.sum @@ -345,6 +345,8 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= From a36d9bcaced941df4f791321f1ff7568e4930d00 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Thu, 4 May 2023 08:33:48 -0500 Subject: [PATCH 5/6] enhance: switch to adhocore/gronx --- database/types/schedule.go | 9 +++------ go.mod | 1 + go.sum | 2 ++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/database/types/schedule.go b/database/types/schedule.go index 9a2e58ecc..6ed68df04 100644 --- a/database/types/schedule.go +++ b/database/types/schedule.go @@ -8,9 +8,9 @@ import ( "database/sql" "errors" + "github.com/adhocore/gronx" "github.com/go-vela/server/api/types" "github.com/go-vela/types/library" - "github.com/robfig/cron/v3" ) var ( @@ -25,9 +25,6 @@ var ( // ErrInvalidScheduleEntry defines the error type when a Schedule type has an invalid Entry field provided. ErrInvalidScheduleEntry = errors.New("invalid schedule entry provided") - - // scheduleParser defines the parser used for validating the Entry field for the Schedule type. - scheduleParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) ) // Schedule is the database representation of a schedule for a repo. @@ -128,8 +125,8 @@ func (s *Schedule) Validate() error { return ErrEmptyScheduleEntry } - _, err := scheduleParser.Parse(s.Entry.String) - if err != nil { + gron := gronx.New() + if !gron.IsValid(s.Entry.String) { return ErrInvalidScheduleEntry } diff --git a/go.mod b/go.mod index c785314ab..eae710da3 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/sprig/v3 v3.2.3 + github.com/adhocore/gronx v1.6.2 github.com/alicebob/miniredis/v2 v2.30.2 github.com/aws/aws-sdk-go v1.44.248 github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3 diff --git a/go.sum b/go.sum index 14579af4c..0ebb472c2 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/adhocore/gronx v1.6.2 h1:/Pg6cuHFJmUGRIYWhRFjb6iL9fdzNmoMPj+/r6L01KU= +github.com/adhocore/gronx v1.6.2/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= From f296d4d8ed541c76348a1c9b36f188515f36e709 Mon Sep 17 00:00:00 2001 From: JordanBrockopp Date: Thu, 4 May 2023 08:36:52 -0500 Subject: [PATCH 6/6] chore: update go deps --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index eae710da3..65c5b768e 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.15.0 github.com/redis/go-redis/v9 v9.0.3 - github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.0 github.com/spf13/afero v1.9.5 github.com/urfave/cli/v2 v2.25.1 diff --git a/go.sum b/go.sum index 0ebb472c2..f8198fced 100644 --- a/go.sum +++ b/go.sum @@ -347,8 +347,6 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=