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"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
|
"github.com/portainer/portainer/pkg/edge"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type edgeStackFromFileUploadPayload struct {
|
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
|
Name string
|
||||||
StackFileContent []byte
|
StackFileContent []byte
|
||||||
EdgeGroups []portainer.EdgeGroupID
|
EdgeGroups []portainer.EdgeGroupID
|
||||||
|
@ -32,6 +38,10 @@ func (payload *edgeStackFromFileUploadPayload) Validate(r *http.Request) error {
|
||||||
}
|
}
|
||||||
payload.Name = name
|
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")
|
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperrors.NewInvalidPayloadError("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
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
|
// @security jwt
|
||||||
// @accept multipart/form-data
|
// @accept multipart/form-data
|
||||||
// @produce json
|
// @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 file formData file true "Content of the Stack file"
|
||||||
// @param EdgeGroups formData string true "JSON stringified array of Edge Groups ids"
|
// @param EdgeGroups formData string true "JSON stringified array of Edge Groups ids"
|
||||||
// @param DeploymentType formData int true "deploy type 0 - 'compose', 1 - 'kubernetes'"
|
// @param DeploymentType formData int true "deploy type 0 - 'compose', 1 - 'kubernetes'"
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
|
"github.com/portainer/portainer/pkg/edge"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -17,7 +18,11 @@ import (
|
||||||
|
|
||||||
type edgeStackFromGitRepositoryPayload struct {
|
type edgeStackFromGitRepositoryPayload struct {
|
||||||
// Name of the stack
|
// 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
|
// URL of a Git repository hosting the Stack file
|
||||||
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
|
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
|
||||||
// Reference name of a Git repository hosting the Stack file
|
// 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")
|
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) {
|
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
|
||||||
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
|
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/dataservices"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
|
"github.com/portainer/portainer/pkg/edge"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -15,7 +16,11 @@ import (
|
||||||
|
|
||||||
type edgeStackFromStringPayload struct {
|
type edgeStackFromStringPayload struct {
|
||||||
// Name of the stack
|
// 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
|
// Content of the Stack file
|
||||||
StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"`
|
StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx" validate:"required"`
|
||||||
// List of identifiers of EdgeGroups
|
// List of identifiers of EdgeGroups
|
||||||
|
@ -36,6 +41,10 @@ func (payload *edgeStackFromStringPayload) Validate(r *http.Request) error {
|
||||||
return httperrors.NewInvalidPayloadError("Invalid stack name")
|
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 {
|
if len(payload.StackFileContent) == 0 {
|
||||||
return httperrors.NewInvalidPayloadError("Invalid stack file content")
|
return httperrors.NewInvalidPayloadError("Invalid stack file content")
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ func TestCreateAndInspect(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := edgeStackFromStringPayload{
|
payload := edgeStackFromStringPayload{
|
||||||
Name: "Test Stack",
|
Name: "test-stack",
|
||||||
StackFileContent: "stack content",
|
StackFileContent: "stack content",
|
||||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||||
|
@ -161,7 +161,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "EdgeStackDeploymentKubernetes with Docker endpoint",
|
Name: "EdgeStackDeploymentKubernetes with Docker endpoint",
|
||||||
Payload: edgeStackFromStringPayload{
|
Payload: edgeStackFromStringPayload{
|
||||||
Name: "Stack name",
|
Name: "stack-name",
|
||||||
StackFileContent: "content",
|
StackFileContent: "content",
|
||||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||||
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
||||||
|
@ -172,7 +172,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "Empty Stack File Content",
|
Name: "Empty Stack File Content",
|
||||||
Payload: edgeStackFromStringPayload{
|
Payload: edgeStackFromStringPayload{
|
||||||
Name: "Stack name",
|
Name: "stack-name",
|
||||||
StackFileContent: "",
|
StackFileContent: "",
|
||||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||||
|
@ -183,7 +183,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
|
||||||
{
|
{
|
||||||
Name: "Clone Git repository error",
|
Name: "Clone Git repository error",
|
||||||
Payload: edgeStackFromGitRepositoryPayload{
|
Payload: edgeStackFromGitRepositoryPayload{
|
||||||
Name: "Stack name",
|
Name: "stack-name",
|
||||||
RepositoryURL: "github.com/portainer/portainer",
|
RepositoryURL: "github.com/portainer/portainer",
|
||||||
RepositoryReferenceName: "ref name",
|
RepositoryReferenceName: "ref name",
|
||||||
RepositoryAuthentication: false,
|
RepositoryAuthentication: false,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetPortainerURLFromEdgeKey returns the portainer URL from an edge key
|
// GetPortainerURLFromEdgeKey returns the portainer URL from an edge key
|
||||||
|
@ -28,3 +29,24 @@ func GetPortainerURLFromEdgeKey(edgeKey string) (string, error) {
|
||||||
|
|
||||||
return keyInfo[0], nil
|
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