diff --git a/cmd/server/pactasrv/BUILD.bazel b/cmd/server/pactasrv/BUILD.bazel index 8f7bdbe..ca8eb24 100644 --- a/cmd/server/pactasrv/BUILD.bazel +++ b/cmd/server/pactasrv/BUILD.bazel @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "pactasrv", @@ -13,6 +13,7 @@ go_library( "initiative_invitation.go", "initiative_portfolio_relationship.go", "initiative_user_relationship.go", + "limits.go", "pacta_version.go", "pactasrv.go", "parallel.go", @@ -39,3 +40,9 @@ go_library( "@org_uber_go_zap//zapcore", ], ) + +go_test( + name = "pactasrv_test", + srcs = ["limits_test.go"], + embed = [":pactasrv"], +) diff --git a/cmd/server/pactasrv/analysis.go b/cmd/server/pactasrv/analysis.go index 9edf454..dde1360 100644 --- a/cmd/server/pactasrv/analysis.go +++ b/cmd/server/pactasrv/analysis.go @@ -92,6 +92,12 @@ func (s *Server) FindAnalysisById(ctx context.Context, request api.FindAnalysisB // Updates writable analysis properties // (PATCH /analysis/{id}) func (s *Server) UpdateAnalysis(ctx context.Context, request api.UpdateAnalysisRequestObject) (api.UpdateAnalysisResponseObject, error) { + if err := anyError( + checkStringLimitSmallPtr("name", request.Body.Name), + checkStringLimitMediumPtr("description", request.Body.Description), + ); err != nil { + return nil, err + } id := pacta.AnalysisID(request.Id) if err := s.analysisDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Update); err != nil { return nil, err @@ -150,6 +156,12 @@ func (s *Server) UpdateAnalysisArtifact(ctx context.Context, request api.UpdateA // Requests an analysis be run // (POST /run-analysis) func (s *Server) RunAnalysis(ctx context.Context, request api.RunAnalysisRequestObject) (api.RunAnalysisResponseObject, error) { + if err := anyError( + checkStringLimitSmall("name", request.Body.Name), + checkStringLimitMedium("description", request.Body.Description), + ); err != nil { + return nil, err + } actorInfo, err := s.getActorInfoOrErrIfAnon(ctx) if err != nil { return nil, err diff --git a/cmd/server/pactasrv/incomplete_upload.go b/cmd/server/pactasrv/incomplete_upload.go index 2c524c7..183339e 100644 --- a/cmd/server/pactasrv/incomplete_upload.go +++ b/cmd/server/pactasrv/incomplete_upload.go @@ -67,6 +67,12 @@ func (s *Server) FindIncompleteUploadById(ctx context.Context, request api.FindI // Updates incomplete upload properties // (PATCH /incomplete-upload/{id}) func (s *Server) UpdateIncompleteUpload(ctx context.Context, request api.UpdateIncompleteUploadRequestObject) (api.UpdateIncompleteUploadResponseObject, error) { + if err := anyError( + checkStringLimitSmallPtr("name", request.Body.Name), + checkStringLimitMediumPtr("description", request.Body.Description), + ); err != nil { + return nil, err + } id := pacta.IncompleteUploadID(request.Id) mutations := []db.UpdateIncompleteUploadFn{} if request.Body.Name != nil { diff --git a/cmd/server/pactasrv/initiative.go b/cmd/server/pactasrv/initiative.go index 4518c8e..4f65c1b 100644 --- a/cmd/server/pactasrv/initiative.go +++ b/cmd/server/pactasrv/initiative.go @@ -15,6 +15,15 @@ import ( // Creates a initiative // (POST /initiatives) func (s *Server) CreateInitiative(ctx context.Context, request api.CreateInitiativeRequestObject) (api.CreateInitiativeResponseObject, error) { + if err := anyError( + checkStringLimitSmall("id", request.Body.Id), + checkStringLimitSmallPtr("affiliation", request.Body.Affiliation), + checkStringLimitSmall("name", request.Body.Name), + checkStringLimitMediumPtr("internal_description", request.Body.InternalDescription), + checkStringLimitMediumPtr("public_description", request.Body.PublicDescription), + ); err != nil { + return nil, err + } actorInfo, err := s.getActorInfoOrErrIfAnon(ctx) if err != nil { return nil, err @@ -49,6 +58,14 @@ func (s *Server) CreateInitiative(ctx context.Context, request api.CreateInitiat // Updates an initiative // (PATCH /initiative/{id}) func (s *Server) UpdateInitiative(ctx context.Context, request api.UpdateInitiativeRequestObject) (api.UpdateInitiativeResponseObject, error) { + if err := anyError( + checkStringLimitSmallPtr("affiliation", request.Body.Affiliation), + checkStringLimitSmallPtr("name", request.Body.Name), + checkStringLimitMediumPtr("internal_description", request.Body.InternalDescription), + checkStringLimitMediumPtr("public_description", request.Body.PublicDescription), + ); err != nil { + return nil, err + } id := pacta.InitiativeID(request.Id) if _, err := s.initiativeDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Update); err != nil { return nil, err diff --git a/cmd/server/pactasrv/initiative_invitation.go b/cmd/server/pactasrv/initiative_invitation.go index a2dd52e..738b1ec 100644 --- a/cmd/server/pactasrv/initiative_invitation.go +++ b/cmd/server/pactasrv/initiative_invitation.go @@ -16,6 +16,9 @@ import ( // Creates an initiative invitation // (POST /initiative-invitation) func (s *Server) CreateInitiativeInvitation(ctx context.Context, request api.CreateInitiativeInvitationRequestObject) (api.CreateInitiativeInvitationResponseObject, error) { + if err := checkStringLimitSmall("id", request.Body.Id); err != nil { + return nil, err + } ii, err := conv.InitiativeInvitationFromOAPI(request.Body) if err != nil { return nil, err diff --git a/cmd/server/pactasrv/limits.go b/cmd/server/pactasrv/limits.go new file mode 100644 index 0000000..812b592 --- /dev/null +++ b/cmd/server/pactasrv/limits.go @@ -0,0 +1,85 @@ +package pactasrv + +import ( + "fmt" + "math" + + "github.com/RMI/pacta/oapierr" + "go.uber.org/zap" +) + +func checkStringLimitMediumPtr(site string, value *string) error { + if value == nil { + return nil + } + return checkStringLimitMedium(site, *value) +} + +func checkStringLimitMedium(site string, value string) error { + return checkStringLimit(site, value, 10000) +} + +func checkStringLimitSmallPtr(site string, value *string) error { + if value == nil { + return nil + } + return checkStringLimitSmall(site, *value) +} + +func checkStringLimitSmall(site string, value string) error { + return checkStringLimit(site, value, 1000) +} + +func checkStringLimit(site string, value string, byteLimit int) error { + byteLength := len(value) + return checkLimit( + site, + byteLength, formatByteSize(byteLength), + byteLimit, formatByteSize(byteLimit)) +} + +func checkIntLimit(site string, value int, limit int) error { + return checkLimit(site, value, fmt.Sprintf("%d", value), limit, fmt.Sprintf("%d", limit)) +} + +func checkLimit(site string, value int, valueStr string, limit int, limitStr string) error { + if value > limit { + return oapierr.BadRequest( + "Input too large", + zap.String("site", site), + zap.Int("value", value), + zap.Int("limit", limit), + ).WithMessage( + fmt.Sprintf("the input for %s is too large (%s exceeds the limit, %s)", + site, + valueStr, + limitStr), + ).WithErrorID("INPUT_EXCEEDS_LIMIT") + } + return nil +} + +func formatByteSize(bytes int) string { + if bytes <= 0 { + return "" + } + if bytes < 1000 { + return fmt.Sprintf("%d %s", bytes, dataSizes[0]) + } + k := 1000.0 + i := int(math.Floor(math.Log(float64(bytes)) / math.Log(k))) + return fmt.Sprintf("%.2f %s", float64(bytes)/math.Pow(k, float64(i)), dataSizes[i]) +} + +var dataSizes []string = []string{ + "Bytes", "kB", "MB", "GB", "TB", +} + +func anyError(errs ...error) error { + for _, err := range errs { + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/server/pactasrv/limits_test.go b/cmd/server/pactasrv/limits_test.go new file mode 100644 index 0000000..b06cf9f --- /dev/null +++ b/cmd/server/pactasrv/limits_test.go @@ -0,0 +1,27 @@ +package pactasrv + +import "testing" + +func TestFormatByteSize(t *testing.T) { + tests := []struct { + name string + bytes int + expected string + }{ + {"TestZeroBytes", 0, ""}, + {"TestNegativeBytes", -100, ""}, + {"TestBytes", 7, "7 Bytes"}, + {"TestKilobyte", 2321, "2.32 kB"}, + {"TestMegabyte", 1004999, "1.00 MB"}, + {"TestGigabyte", 40005100000, "40.01 GB"}, + {"TestTerabyte", 100000000000000, "100.00 TB"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := formatByteSize(tt.bytes); got != tt.expected { + t.Errorf("formatByteSize(%d) = %v, want %v", tt.bytes, got, tt.expected) + } + }) + } +} diff --git a/cmd/server/pactasrv/pacta_version.go b/cmd/server/pactasrv/pacta_version.go index 76dc0ac..422fd4d 100644 --- a/cmd/server/pactasrv/pacta_version.go +++ b/cmd/server/pactasrv/pacta_version.go @@ -51,6 +51,13 @@ func (s *Server) ListPactaVersions(ctx context.Context, request api.ListPactaVer // Creates a PACTA version // (POST /pacta-versions) func (s *Server) CreatePactaVersion(ctx context.Context, request api.CreatePactaVersionRequestObject) (api.CreatePactaVersionResponseObject, error) { + if err := anyError( + checkStringLimitSmall("name", request.Body.Name), + checkStringLimitSmall("digest", request.Body.Digest), + checkStringLimitMedium("description", request.Body.Description), + ); err != nil { + return nil, err + } actorInfo, err := s.getActorInfoOrErrIfAnon(ctx) if err != nil { return nil, err @@ -80,6 +87,13 @@ func (s *Server) CreatePactaVersion(ctx context.Context, request api.CreatePacta // Updates a PACTA version // (PATCH /pacta-version/{id}) func (s *Server) UpdatePactaVersion(ctx context.Context, request api.UpdatePactaVersionRequestObject) (api.UpdatePactaVersionResponseObject, error) { + if err := anyError( + checkStringLimitSmallPtr("name", request.Body.Name), + checkStringLimitSmallPtr("digest", request.Body.Digest), + checkStringLimitMediumPtr("description", request.Body.Description), + ); err != nil { + return nil, err + } id := pacta.PACTAVersionID(request.Id) if err := s.pactaVersionAuthz(ctx, id, pacta.AuditLogAction_Update); err != nil { return nil, err diff --git a/cmd/server/pactasrv/portfolio.go b/cmd/server/pactasrv/portfolio.go index 594acbd..dcb7e46 100644 --- a/cmd/server/pactasrv/portfolio.go +++ b/cmd/server/pactasrv/portfolio.go @@ -79,6 +79,12 @@ func (s *Server) FindPortfolioById(ctx context.Context, request api.FindPortfoli // Updates portfolio properties // (PATCH /portfolio/{id}) func (s *Server) UpdatePortfolio(ctx context.Context, request api.UpdatePortfolioRequestObject) (api.UpdatePortfolioResponseObject, error) { + if err := anyError( + checkStringLimitSmallPtr("name", request.Body.Name), + checkStringLimitMediumPtr("description", request.Body.Description), + ); err != nil { + return nil, err + } id := pacta.PortfolioID(request.Id) if err := s.portfolioDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Update); err != nil { return nil, err diff --git a/cmd/server/pactasrv/portfolio_group.go b/cmd/server/pactasrv/portfolio_group.go index dc49fba..6400b6b 100644 --- a/cmd/server/pactasrv/portfolio_group.go +++ b/cmd/server/pactasrv/portfolio_group.go @@ -60,6 +60,12 @@ func (s *Server) ListPortfolioGroups(ctx context.Context, request api.ListPortfo // Creates a portfolio group // (POST /portfolio-groups) func (s *Server) CreatePortfolioGroup(ctx context.Context, request api.CreatePortfolioGroupRequestObject) (api.CreatePortfolioGroupResponseObject, error) { + if err := anyError( + checkStringLimitSmall("name", request.Body.Name), + checkStringLimitMedium("description", request.Body.Description), + ); err != nil { + return nil, err + } actorInfo, err := s.getActorInfoOrErrIfAnon(ctx) if err != nil { return nil, err @@ -87,6 +93,12 @@ func (s *Server) CreatePortfolioGroup(ctx context.Context, request api.CreatePor // Updates portfolio group properties // (PATCH /portfolio-group/{id}) func (s *Server) UpdatePortfolioGroup(ctx context.Context, request api.UpdatePortfolioGroupRequestObject) (api.UpdatePortfolioGroupResponseObject, error) { + if err := anyError( + checkStringLimitSmallPtr("name", request.Body.Name), + checkStringLimitMediumPtr("description", request.Body.Description), + ); err != nil { + return nil, err + } id := pacta.PortfolioGroupID(request.Id) if err := s.portfolioGroupAuthz(ctx, id, pacta.AuditLogAction_Update); err != nil { return nil, err diff --git a/cmd/server/pactasrv/upload.go b/cmd/server/pactasrv/upload.go index 08ee393..176d0fd 100644 --- a/cmd/server/pactasrv/upload.go +++ b/cmd/server/pactasrv/upload.go @@ -19,7 +19,9 @@ import ( // Starts the process of uploading one or more portfolio files // (POST /portfolio-upload) func (s *Server) StartPortfolioUpload(ctx context.Context, request api.StartPortfolioUploadRequestObject) (api.StartPortfolioUploadResponseObject, error) { - // TODO(#71) Implement basic limits + if err := checkIntLimit("number_of_uploaded_files", len(request.Body.Items), 25); err != nil { + return nil, err + } actorInfo, err := s.getActorInfoOrErrIfAnon(ctx) if err != nil { return nil, err @@ -35,14 +37,13 @@ func (s *Server) StartPortfolioUpload(ctx context.Context, request api.StartPort properties.EngagementStrategy = conv.OptionalBoolFromOAPI(request.Body.PropertyEngagementStrategy) n := len(request.Body.Items) - if n > 25 { - // TODO(#71) Implement basic limits - return nil, oapierr.BadRequest("too many items") - } blobs := make([]*pacta.Blob, n) respItems := make([]api.StartPortfolioUploadRespItem, n) for i, item := range request.Body.Items { fn := item.FileName + if err := checkStringLimitSmall("file_name", fn); err != nil { + return nil, err + } extStr := filepath.Ext(fn) ft, err := pacta.ParseFileType(extStr) if err != nil { diff --git a/cmd/server/pactasrv/user.go b/cmd/server/pactasrv/user.go index ecb346d..a6ff307 100644 --- a/cmd/server/pactasrv/user.go +++ b/cmd/server/pactasrv/user.go @@ -34,6 +34,11 @@ func (s *Server) FindUserById(ctx context.Context, request api.FindUserByIdReque // Updates user properties // (PATCH /user/{id}) func (s *Server) UpdateUser(ctx context.Context, request api.UpdateUserRequestObject) (api.UpdateUserResponseObject, error) { + if err := anyError( + checkStringLimitSmallPtr("name", request.Body.Name), + ); err != nil { + return nil, err + } id := pacta.UserID(request.Id) if err := s.userDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_Update); err != nil { return nil, err