mirror of https://github.com/portainer/portainer
				
				
				
			fix(edgestack): validate edge stack name for api [BE-11365] (#222)
							parent
							
								
									05e872337a
								
							
						
					
					
						commit
						40c7742e46
					
				| 
						 | 
				
			
			@ -6,12 +6,18 @@ import (
 | 
			
		|||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/dataservices"
 | 
			
		||||
	httperrors "github.com/portainer/portainer/api/http/errors"
 | 
			
		||||
	"github.com/portainer/portainer/pkg/edge"
 | 
			
		||||
	"github.com/portainer/portainer/pkg/libhttp/request"
 | 
			
		||||
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type edgeStackFromFileUploadPayload struct {
 | 
			
		||||
	// Name of the stack
 | 
			
		||||
	// Max length: 255
 | 
			
		||||
	// Name must only contains lowercase characters, numbers, hyphens, or underscores
 | 
			
		||||
	// Name must start with a lowercase character or number
 | 
			
		||||
	// Example: stack-name or stack_123 or stackName
 | 
			
		||||
	Name             string
 | 
			
		||||
	StackFileContent []byte
 | 
			
		||||
	EdgeGroups       []portainer.EdgeGroupID
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +38,10 @@ func (payload *edgeStackFromFileUploadPayload) Validate(r *http.Request) error {
 | 
			
		|||
	}
 | 
			
		||||
	payload.Name = name
 | 
			
		||||
 | 
			
		||||
	if !edge.IsValidEdgeStackName(payload.Name) {
 | 
			
		||||
		return httperrors.NewInvalidPayloadError("Invalid stack name. Stack name must only consist of lowercase alpha characters, numbers, hyphens, or underscores as well as start with a lowercase character or number")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return httperrors.NewInvalidPayloadError("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +85,7 @@ func (payload *edgeStackFromFileUploadPayload) Validate(r *http.Request) error {
 | 
			
		|||
// @security jwt
 | 
			
		||||
// @accept multipart/form-data
 | 
			
		||||
// @produce json
 | 
			
		||||
// @param Name formData string true "Name of the stack"
 | 
			
		||||
// @param Name formData string true "Name of the stack. it must only consist of lowercase alphanumeric characters, hyphens, or underscores as well as start with a letter or number"
 | 
			
		||||
// @param file formData file true "Content of the Stack file"
 | 
			
		||||
// @param EdgeGroups formData string true "JSON stringified array of Edge Groups ids"
 | 
			
		||||
// @param DeploymentType formData int true "deploy type 0 - 'compose', 1 - 'kubernetes'"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import (
 | 
			
		|||
	"github.com/portainer/portainer/api/filesystem"
 | 
			
		||||
	gittypes "github.com/portainer/portainer/api/git/types"
 | 
			
		||||
	httperrors "github.com/portainer/portainer/api/http/errors"
 | 
			
		||||
	"github.com/portainer/portainer/pkg/edge"
 | 
			
		||||
	"github.com/portainer/portainer/pkg/libhttp/request"
 | 
			
		||||
 | 
			
		||||
	"github.com/asaskevich/govalidator"
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +18,11 @@ import (
 | 
			
		|||
 | 
			
		||||
type edgeStackFromGitRepositoryPayload struct {
 | 
			
		||||
	// Name of the stack
 | 
			
		||||
	Name string `example:"myStack" validate:"required"`
 | 
			
		||||
	// Max length: 255
 | 
			
		||||
	// Name must only contains lowercase characters, numbers, hyphens, or underscores
 | 
			
		||||
	// Name must start with a lowercase character or number
 | 
			
		||||
	// Example: stack-name or stack_123 or stackName
 | 
			
		||||
	Name string `example:"stack-name" validate:"required"`
 | 
			
		||||
	// URL of a Git repository hosting the Stack file
 | 
			
		||||
	RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
 | 
			
		||||
	// Reference name of a Git repository hosting the Stack file
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +55,10 @@ func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) erro
 | 
			
		|||
		return httperrors.NewInvalidPayloadError("Invalid stack name")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !edge.IsValidEdgeStackName(payload.Name) {
 | 
			
		||||
		return httperrors.NewInvalidPayloadError("Invalid stack name. Stack name must only consist of lowercase alpha characters, numbers, hyphens, or underscores as well as start with a lowercase character or number")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
 | 
			
		||||
		return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import (
 | 
			
		|||
	"github.com/portainer/portainer/api/dataservices"
 | 
			
		||||
	"github.com/portainer/portainer/api/filesystem"
 | 
			
		||||
	httperrors "github.com/portainer/portainer/api/http/errors"
 | 
			
		||||
	"github.com/portainer/portainer/pkg/edge"
 | 
			
		||||
	"github.com/portainer/portainer/pkg/libhttp/request"
 | 
			
		||||
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +16,11 @@ import (
 | 
			
		|||
 | 
			
		||||
type edgeStackFromStringPayload struct {
 | 
			
		||||
	// Name of the stack
 | 
			
		||||
	Name string `example:"myStack" validate:"required"`
 | 
			
		||||
	// Max length: 255
 | 
			
		||||
	// Name must only contains lowercase characters, numbers, hyphens, or underscores
 | 
			
		||||
	// Name must start with a lowercase character or number
 | 
			
		||||
	// Example: stack-name or stack_123 or stackName
 | 
			
		||||
	Name string `example:"stack-name" validate:"required"`
 | 
			
		||||
	// Content of the Stack file
 | 
			
		||||
	StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"`
 | 
			
		||||
	// List of identifiers of EdgeGroups
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +41,10 @@ func (payload *edgeStackFromStringPayload) Validate(r *http.Request) error {
 | 
			
		|||
		return httperrors.NewInvalidPayloadError("Invalid stack name")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !edge.IsValidEdgeStackName(payload.Name) {
 | 
			
		||||
		return httperrors.NewInvalidPayloadError("Invalid stack name. Stack name must only consist of lowercase alpha characters, numbers, hyphens, or underscores as well as start with a lowercase character or number")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(payload.StackFileContent) == 0 {
 | 
			
		||||
		return httperrors.NewInvalidPayloadError("Invalid stack file content")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ func TestCreateAndInspect(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	payload := edgeStackFromStringPayload{
 | 
			
		||||
		Name:             "Test Stack",
 | 
			
		||||
		Name:             "test-stack",
 | 
			
		||||
		StackFileContent: "stack content",
 | 
			
		||||
		EdgeGroups:       []portainer.EdgeGroupID{1},
 | 
			
		||||
		DeploymentType:   portainer.EdgeStackDeploymentCompose,
 | 
			
		||||
| 
						 | 
				
			
			@ -161,7 +161,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
 | 
			
		|||
		{
 | 
			
		||||
			Name: "EdgeStackDeploymentKubernetes with Docker endpoint",
 | 
			
		||||
			Payload: edgeStackFromStringPayload{
 | 
			
		||||
				Name:             "Stack name",
 | 
			
		||||
				Name:             "stack-name",
 | 
			
		||||
				StackFileContent: "content",
 | 
			
		||||
				EdgeGroups:       []portainer.EdgeGroupID{1},
 | 
			
		||||
				DeploymentType:   portainer.EdgeStackDeploymentKubernetes,
 | 
			
		||||
| 
						 | 
				
			
			@ -172,7 +172,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
 | 
			
		|||
		{
 | 
			
		||||
			Name: "Empty Stack File Content",
 | 
			
		||||
			Payload: edgeStackFromStringPayload{
 | 
			
		||||
				Name:             "Stack name",
 | 
			
		||||
				Name:             "stack-name",
 | 
			
		||||
				StackFileContent: "",
 | 
			
		||||
				EdgeGroups:       []portainer.EdgeGroupID{1},
 | 
			
		||||
				DeploymentType:   portainer.EdgeStackDeploymentCompose,
 | 
			
		||||
| 
						 | 
				
			
			@ -183,7 +183,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
 | 
			
		|||
		{
 | 
			
		||||
			Name: "Clone Git repository error",
 | 
			
		||||
			Payload: edgeStackFromGitRepositoryPayload{
 | 
			
		||||
				Name:                     "Stack name",
 | 
			
		||||
				Name:                     "stack-name",
 | 
			
		||||
				RepositoryURL:            "github.com/portainer/portainer",
 | 
			
		||||
				RepositoryReferenceName:  "ref name",
 | 
			
		||||
				RepositoryAuthentication: false,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import (
 | 
			
		|||
	"errors"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetPortainerURLFromEdgeKey returns the portainer URL from an edge key
 | 
			
		||||
| 
						 | 
				
			
			@ -28,3 +29,24 @@ func GetPortainerURLFromEdgeKey(edgeKey string) (string, error) {
 | 
			
		|||
 | 
			
		||||
	return keyInfo[0], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsValidEdgeStackName validates an edge stack name
 | 
			
		||||
// Edge stack name must be between 1 and 255 characters long
 | 
			
		||||
// and can only contain lowercase letters, digits, hyphens and underscores
 | 
			
		||||
// Edge stack name must start with either a lowercase letter or a digit
 | 
			
		||||
func IsValidEdgeStackName(name string) bool {
 | 
			
		||||
	if len(name) == 0 || len(name) > 255 {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !unicode.IsLower(rune(name[0])) && !unicode.IsDigit(rune(name[0])) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, r := range name {
 | 
			
		||||
		if !(unicode.IsLower(r) || unicode.IsDigit(r) || r == '-' || r == '_') {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,3 +27,34 @@ func TestGetPortainerURLFromEdgeKey(t *testing.T) {
 | 
			
		|||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIsValidEdgeStackName(t *testing.T) {
 | 
			
		||||
	f := func(name string, expected bool) {
 | 
			
		||||
		if IsValidEdgeStackName(name) != expected {
 | 
			
		||||
			t.Fatalf("expected %v, found %v", expected, IsValidEdgeStackName(name))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	f("edge-stack", true)
 | 
			
		||||
	f("edge_stack", true)
 | 
			
		||||
	f("edgestack", true)
 | 
			
		||||
	f("edgestack11", true)
 | 
			
		||||
	f("111", true)
 | 
			
		||||
	f("111edgestack", true)
 | 
			
		||||
	f("edge#stack", false)
 | 
			
		||||
	f("edge stack", false)
 | 
			
		||||
	f("Edge_stack", false)
 | 
			
		||||
	f("EdgeStack", false)
 | 
			
		||||
	f("-edgestack", false)
 | 
			
		||||
	f("_edgestack", false)
 | 
			
		||||
	f("#edgestack", false)
 | 
			
		||||
	f("/edgestack", false)
 | 
			
		||||
	f("#edgestack", false)
 | 
			
		||||
	f("édgestack", false)
 | 
			
		||||
	f("", false)
 | 
			
		||||
	f(" ", false)
 | 
			
		||||
	f("-", false)
 | 
			
		||||
	f("_", false)
 | 
			
		||||
	f("E", false)
 | 
			
		||||
	f("eedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackdgeedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackedgestackstack", false)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue