diff --git a/internal/delivery/http/handler.go b/internal/delivery/http/handler.go index 2e053ae5..ffb133b5 100644 --- a/internal/delivery/http/handler.go +++ b/internal/delivery/http/handler.go @@ -71,6 +71,22 @@ func UnmarshalRequestInput(r *http.Request, in any) error { return nil } +// Http Request가 아닌 경우에도 domain 객체 validate가 필요한 경우 호출 +// 예를 들어 정책의 match를 RawYaml로 전달받았을 경우 이를 domain.Match 객체로 unmarshalling 한 후 domain.Match를 이용해서 validate 가능 +func ValidateDomainObject(in any) error { + err := validate.Struct(in) + if err != nil { + var valErrs validator_.ValidationErrors + if errors.As(err, &valErrs) { + for _, e := range valErrs { + return httpErrors.NewBadRequestError(err, "", e.Translate(trans)) + } + } + } + + return nil +} + /* func (h *APIHandler) GetClientFromClusterId(clusterId string) (*kubernetes.Clientset, error) { const prefix = "CACHE_KEY_KUBE_CLIENT_" diff --git a/internal/delivery/http/policy.go b/internal/delivery/http/policy.go index 412770d8..9a45cd31 100644 --- a/internal/delivery/http/policy.go +++ b/internal/delivery/http/policy.go @@ -93,6 +93,11 @@ func (h *PolicyHandler) CreatePolicy(w http.ResponseWriter, r *http.Request) { ErrorJSON(w, r, httpErrors.NewBadRequestError(fmt.Errorf("match yaml error: %s", err), "P_INVALID_MATCH", "")) return } + + if err := ValidateDomainObject(match); err != nil { + ErrorJSON(w, r, err) + return + } } if len(input.PolicyResourceName) > 0 { @@ -180,6 +185,11 @@ func (h *PolicyHandler) UpdatePolicy(w http.ResponseWriter, r *http.Request) { ErrorJSON(w, r, httpErrors.NewBadRequestError(fmt.Errorf("match yaml error: %s", err), "P_INVALID_MATCH", "")) return } + + if err := ValidateDomainObject(match); err != nil { + ErrorJSON(w, r, err) + return + } } var templateId *uuid.UUID = nil diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 212a6394..07b288da 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -2,16 +2,25 @@ package validator import ( "regexp" + "strings" "unicode/utf8" "github.com/go-playground/locales/en" ut "github.com/go-playground/universal-translator" validator "github.com/go-playground/validator/v10" en_translations "github.com/go-playground/validator/v10/translations/en" + "github.com/openinfradev/tks-api/pkg/domain" "github.com/opentracing/opentracing-go/log" ) -const REGEX_RFC1123 = `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$` +const ( + REGEX_RFC1123 = `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$` + REGEX_SIMPLE_SEMVER = `^v\d+\.\d+\.\d+$` + REGEX_PASCAL_CASE = `^([A-Z][a-z\d]+)+$` // 대문자로 시작하는 camel case(pascal case or upper camel case)를 표현한 정규식 + REGEX_RFC1123_DNS_LABEL = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" + REGEX_RESOURCE_NAME = `^` + REGEX_RFC1123_DNS_LABEL + "$" + REGEX_RFC1123_SUBDOMAIN = `^` + REGEX_RFC1123_DNS_LABEL + `(\.` + REGEX_RFC1123_DNS_LABEL + `)*$` +) func NewValidator() (*validator.Validate, *ut.UniversalTranslator) { en := en.New() @@ -27,6 +36,11 @@ func NewValidator() (*validator.Validate, *ut.UniversalTranslator) { // register custom validator _ = v.RegisterValidation("rfc1123", validateRfc1123) _ = v.RegisterValidation("name", validateName) + _ = v.RegisterValidation("version", validateVersion) + _ = v.RegisterValidation("pascalcase", validatePascalCase) + _ = v.RegisterValidation("resourcename", validateResourceName) + _ = v.RegisterValidation("matchnamespace", validateMatchNamespace) + _ = v.RegisterValidation("matchkinds", validateMatchKinds) // register custom error _ = v.RegisterTranslation("required", trans, func(ut ut.Translator) error { @@ -65,3 +79,129 @@ func validateName(fl validator.FieldLevel) bool { return utf8.RuneCountInString(fl.Field().String()) <= 30 } + +func validateVersion(fl validator.FieldLevel) bool { + if fl.Field().String() == "" { + return false + } + + r, _ := regexp.Compile(REGEX_SIMPLE_SEMVER) + return r.MatchString(fl.Field().String()) +} + +func validatePascalCase(fl validator.FieldLevel) bool { + if fl.Field().String() == "" { + return false + } + + r, _ := regexp.Compile(REGEX_PASCAL_CASE) + return r.MatchString(fl.Field().String()) +} + +func validateResourceName(fl validator.FieldLevel) bool { + if fl.Field().String() == "" { + return false + } + + r, _ := regexp.Compile(REGEX_RESOURCE_NAME) + return r.MatchString(fl.Field().String()) +} + +func validateMatchKinds(fl validator.FieldLevel) bool { + kinds, ok := fl.Field().Interface().([]domain.Kinds) + if !ok { + return false + } + + for _, kind := range kinds { + if ok := validateMatchKindAPIGroup(kind.APIGroups) && validateMatchKindKind(kind.Kinds); !ok { + return false + } + } + + return true +} + +func validateMatchKindAPIGroup(apigroups []string) bool { + if len(apigroups) == 0 { + return true + } + + containsWildcard := false + + r, _ := regexp.Compile(REGEX_RFC1123_SUBDOMAIN) + + for _, apigroup := range apigroups { + if apigroup == "*" || apigroup == "" { + containsWildcard = true + } else { + if !r.MatchString(apigroup) { + return false + } + } + } + + if containsWildcard && len(apigroups) != 1 { + return false + } + + return true +} + +func validateMatchKindKind(kinds []string) bool { + if len(kinds) == 0 { + return true + } + + containsWildcard := false + + r, _ := regexp.Compile(REGEX_PASCAL_CASE) + + for _, kind := range kinds { + if kind == "*" || kind == "" { + containsWildcard = true + } else { + if !r.MatchString(kind) { + return false + } + } + } + + if containsWildcard && len(kinds) != 1 { + return false + } + + return true +} + +func validateMatchNamespace(fl validator.FieldLevel) bool { + namespaces, ok := fl.Field().Interface().([]string) + if !ok { + return false + } + + if len(namespaces) == 0 { + return true + } + + containsWildcard := false + + r, _ := regexp.Compile(REGEX_RESOURCE_NAME) + + for _, namespace := range namespaces { + if namespace == "*" || namespace == "" { + containsWildcard = true + } else { + trimmed := strings.TrimSuffix(strings.TrimPrefix(namespace, "*"), "*") + if !r.MatchString(trimmed) { + return false + } + } + } + + if containsWildcard && len(namespaces) != 1 { + return false + } + + return true +} diff --git a/pkg/domain/admin/policy-template.go b/pkg/domain/admin/policy-template.go index c60a8a35..ab13594c 100644 --- a/pkg/domain/admin/policy-template.go +++ b/pkg/domain/admin/policy-template.go @@ -3,7 +3,6 @@ package admin import ( "time" - "github.com/google/uuid" "github.com/openinfradev/tks-api/pkg/domain" ) @@ -43,9 +42,9 @@ type SimplePolicyTemplateResponse struct { } type CreatePolicyTemplateRequest struct { - TemplateName string `json:"templateName" example:"필수 Label 검사" validate:"name"` - Kind string `json:"kind" example:"K8sRequiredLabels" validate:"required"` - Severity string `json:"severity" enums:"low,medium,high" example:"medium"` + TemplateName string `json:"templateName" validate:"required,name" example:"필수 Label 검사"` + Kind string `json:"kind" example:"K8sRequiredLabels" validate:"required,pascalcase"` + Severity string `json:"severity" validate:"required,oneof=low medium high" enums:"low,medium,high" example:"medium"` Deprecated bool `json:"deprecated" example:"false"` Description string `json:"description,omitempty" example:"이 정책은 ..."` ParametersSchema []*domain.ParameterDef `json:"parametersSchema,omitempty"` @@ -65,46 +64,14 @@ type CreateOrganizationPolicyTemplateReponse struct { ID string `json:"id" example:"d98ef5f1-4a68-4047-a446-2207787ce3ff"` } -type UpdateCommmonPolicyTemplateRequest struct { - TemplateName string `json:"templateName" example:"필수 Label 검사"` - Description string `json:"description,omitempty"` - Severity string `json:"severity" enums:"low,medium,high" example:"medium"` - Deprecated bool `json:"deprecated" example:"false"` - // Tags []string `json:"tags,omitempty"` -} - -type UpdatePolicyTemplateUpdate struct { - ID uuid.UUID - Type string - UpdatorId uuid.UUID - TemplateName *string - Description *string - Severity *string - Deprecated *bool - PermittedOrganizationIds *[]string -} - -func (dto *UpdatePolicyTemplateUpdate) IsNothingToUpdate() bool { - return dto.TemplateName == nil && - dto.Description == nil && - dto.Severity == nil && - dto.Deprecated == nil && - dto.PermittedOrganizationIds == nil -} - type UpdatePolicyTemplateRequest struct { - TemplateName *string `json:"templateName,omitempty" example:"필수 Label 검사"` + TemplateName *string `json:"templateName,omitempty" validate:"required,name" example:"필수 Label 검사"` Description *string `json:"description,omitempty"` - Severity *string `json:"severity,omitempty" enums:"low,medium,high" example:"medium"` + Severity *string `json:"severity,omitempty" validate:"oneof=low medium high" enums:"low,medium,high" example:"medium"` Deprecated *bool `json:"deprecated,omitempty" example:"false"` PermittedOrganizationIds *[]string `json:"permittedOrganizationIds,omitempty"` } -type UpdateOrganizationPolicyTemplateRequest struct { - UpdateCommmonPolicyTemplateRequest - Mandatory bool `json:"mandatory"` -} - type GetPolicyTemplateDeployResponse struct { DeployVersion map[string]string `json:"deployVersion"` } @@ -126,9 +93,9 @@ type GetPolicyTemplateVersionResponse struct { } type CreatePolicyTemplateVersionRequest struct { - VersionUpType string `json:"versionUpType" enums:"major,minor,patch" example:"minor" validate:"required"` - CurrentVersion string `json:"currentVersion" example:"v1.0.0" validate:"required"` - ExpectedVersion string `json:"expectedVersion" example:"v1.1.0" validate:"required"` + VersionUpType string `json:"versionUpType" validate:"required,oneof=major minor patch" enums:"major,minor,patch" example:"minor"` + CurrentVersion string `json:"currentVersion" validate:"required,version" example:"v1.0.0"` + ExpectedVersion string `json:"expectedVersion" validate:"required,version" example:"v1.1.0"` ParametersSchema []*domain.ParameterDef `json:"parametersSchema,omitempty"` // "type: object\nproperties: message:\n type: string\n labels:\n type: array\n items:\n type: object\n properties:\n key:\n type: string\n allowedRegex:\n type: string" diff --git a/pkg/domain/policy-template.go b/pkg/domain/policy-template.go index c24ba0df..59311718 100644 --- a/pkg/domain/policy-template.go +++ b/pkg/domain/policy-template.go @@ -2,8 +2,6 @@ package domain import ( "time" - - "github.com/google/uuid" ) type PolicyTemplateResponse struct { @@ -34,9 +32,9 @@ type SimplePolicyTemplateResponse struct { } type CreatePolicyTemplateRequest struct { - TemplateName string `json:"templateName" example:"필수 Label 검사" validate:"name"` - Kind string `json:"kind" example:"K8sRequiredLabels" validate:"required"` - Severity string `json:"severity" enums:"low,medium,high" example:"medium"` + TemplateName string `json:"templateName" validate:"required,name" example:"필수 Label 검사"` + Kind string `json:"kind" example:"K8sRequiredLabels" validate:"required,pascalcase"` + Severity string `json:"severity" validate:"required,oneof=low medium high" enums:"low,medium,high" example:"medium"` Deprecated bool `json:"deprecated" example:"false"` Description string `json:"description,omitempty" example:"이 정책은 ..."` ParametersSchema []*ParameterDef `json:"parametersSchema,omitempty"` @@ -52,37 +50,10 @@ type CreatePolicyTemplateReponse struct { ID string `json:"id" example:"d98ef5f1-4a68-4047-a446-2207787ce3ff"` } -type UpdateCommmonPolicyTemplateRequest struct { - TemplateName string `json:"templateName" example:"필수 Label 검사"` - Description string `json:"description,omitempty"` - Severity string `json:"severity" enums:"low,medium,high" example:"medium"` - Deprecated bool `json:"deprecated" example:"false"` - // Tags []string `json:"tags,omitempty"` -} - -type UpdatePolicyTemplateUpdate struct { - ID uuid.UUID - Type string - UpdatorId uuid.UUID - TemplateName *string - Description *string - Severity *string - Deprecated *bool - PermittedOrganizationIds *[]string -} - -func (dto *UpdatePolicyTemplateUpdate) IsNothingToUpdate() bool { - return dto.TemplateName == nil && - dto.Description == nil && - dto.Severity == nil && - dto.Deprecated == nil && - dto.PermittedOrganizationIds == nil -} - type UpdatePolicyTemplateRequest struct { - TemplateName *string `json:"templateName,omitempty" example:"필수 Label 검사"` + TemplateName *string `json:"templateName,omitempty" validate:"required,name" example:"필수 Label 검사"` Description *string `json:"description,omitempty"` - Severity *string `json:"severity,omitempty" enums:"low,medium,high" example:"medium"` + Severity *string `json:"severity,omitempty" validate:"oneof=low medium high" enums:"low,medium,high" example:"medium"` Deprecated *bool `json:"deprecated,omitempty" example:"false"` PermittedOrganizationIds *[]string `json:"permittedOrganizationIds,omitempty"` } @@ -100,9 +71,9 @@ type GetPolicyTemplateVersionResponse struct { } type CreatePolicyTemplateVersionRequest struct { - VersionUpType string `json:"versionUpType" enums:"major,minor,patch" example:"minor" validate:"required"` - CurrentVersion string `json:"currentVersion" example:"v1.0.0" validate:"required"` - ExpectedVersion string `json:"expectedVersion" example:"v1.1.0" validate:"required"` + VersionUpType string `json:"versionUpType" validate:"required,oneof=major minor patch" enums:"major,minor,patch" example:"minor"` + CurrentVersion string `json:"currentVersion" validate:"required,version" example:"v1.0.0"` + ExpectedVersion string `json:"expectedVersion" validate:"required,version" example:"v1.1.0"` ParametersSchema []*ParameterDef `json:"parametersSchema,omitempty"` // "type: object\nproperties: message:\n type: string\n labels:\n type: array\n items:\n type: object\n properties:\n key:\n type: string\n allowedRegex:\n type: string" diff --git a/pkg/domain/policy.go b/pkg/domain/policy.go index 8171ad03..a147bfc4 100644 --- a/pkg/domain/policy.go +++ b/pkg/domain/policy.go @@ -6,14 +6,14 @@ import ( ) type Kinds struct { - APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,1,rep,name=apiGroups"` + APIGroups []string `json:"apiGroups,omitempty"` Kinds []string `json:"kinds,omitempty"` } type Match struct { - Namespaces []string `json:"namespaces,omitempty"` - ExcludedNamespaces []string `json:"excludedNamespaces,omitempty"` - Kinds []Kinds `json:"kinds,omitempty"` + Namespaces []string `json:"namespaces,omitempty" validate:"matchnamespace"` + ExcludedNamespaces []string `json:"excludedNamespaces,omitempty" validate:"matchnamespace"` + Kinds []Kinds `json:"kinds,omitempty" validate:"matchkinds"` } func (m *Match) JSON() string { @@ -54,11 +54,11 @@ type CreatePolicyRequest struct { TargetClusterIds []string `json:"targetClusterIds" example:"83bf8081-f0c5-4b31-826d-23f6f366ec90,83bf8081-f0c5-4b31-826d-23f6f366ec90"` Mandatory bool `json:"mandatory"` - PolicyName string `json:"policyName" example:"label 정책"` - PolicyResourceName string `json:"policyResourceName,omitempty" example:"labelpolicy"` + PolicyName string `json:"policyName" validate:"required,name" example:"label 정책"` + PolicyResourceName string `json:"policyResourceName,omitempty" validate:"resourcename" example:"labelpolicy"` Description string `json:"description"` TemplateId string `json:"templateId" example:"d98ef5f1-4a68-4047-a446-2207787ce3ff"` - EnforcementAction string `json:"enforcementAction" enum:"warn,deny,dryrun" example:"deny"` + EnforcementAction string `json:"enforcementAction" validate:"required,oneof=deny dryrun warn" enum:"warn,deny,dryrun" example:"deny"` Parameters string `json:"parameters" example:"{\"key\":\"value\"}"` Match *Match `json:"match,omitempty"` MatchYaml *string `json:"matchYaml,omitempty" example:"namespaces:\r\n- testns1"` @@ -73,10 +73,10 @@ type UpdatePolicyRequest struct { TargetClusterIds *[]string `json:"targetClusterIds,omitempty" example:"83bf8081-f0c5-4b31-826d-23f6f366ec90,83bf8081-f0c5-4b31-826d-23f6f366ec90"` Mandatory *bool `json:"mandatory,omitempty"` - PolicyName *string `json:"policyName,omitempty" example:"label 정책"` + PolicyName *string `json:"policyName,omitempty" validate:"required,name" example:"label 정책"` Description *string `json:"description"` TemplateId *string `json:"templateId,omitempty" example:"d98ef5f1-4a68-4047-a446-2207787ce3ff"` - EnforcementAction *string `json:"enforcementAction,omitempty" enum:"warn,deny,dryrun"` + EnforcementAction *string `json:"enforcementAction" validate:"required,oneof=deny dryrun warn" enum:"warn,deny,dryrun" example:"deny"` Parameters *string `json:"parameters" example:"{\"labels\":{\"key\":\"owner\",\"allowedRegex\":\"test*\"}"` Match *Match `json:"match,omitempty"` MatchYaml *string `json:"matchYaml,omitempty"` @@ -133,8 +133,8 @@ type StackPolicyStatusResponse struct { TemplateName string `json:"templateName" example:"레이블 요구"` TemplateId string `json:"templateId" example:"708d1e5b-4e6f-40e9-87a3-329e2fd051a5"` TemplateDescription string `json:"templateDescription" example:"파라미터로 설정된 레이블 검사"` - TemplateCurrentVersion string `json:"templateCurrentVersion" example:"v1.0.1"` - TemplateLatestVerson string `json:"templateLatestVerson" example:"v1.0.3"` + TemplateCurrentVersion string `json:"templateCurrentVersion" example:"v1.0.1"` + TemplateLatestVerson string `json:"templateLatestVerson" example:"v1.0.3"` } type ListStackPolicyStatusResponse struct { @@ -146,8 +146,8 @@ type GetStackPolicyTemplateStatusResponse struct { TemplateId string `json:"templateId" example:"708d1e5b-4e6f-40e9-87a3-329e2fd051a5"` TemplateDescription string `json:"templateDescription" example:"파라미터로 설정된 레이블 검사"` TemplateMandatory bool `json:"templateMandatory"` - TemplateCurrentVersion string `json:"templateCurrentVersion" example:"v1.0.1"` - TemplateLatestVerson string `json:"templateLatestVerson" example:"v1.0.3"` + TemplateCurrentVersion string `json:"templateCurrentVersion" example:"v1.0.1"` + TemplateLatestVerson string `json:"templateLatestVerson" example:"v1.0.3"` TemplateLatestVersonReleaseDate time.Time `json:"templateLatestVersonReleaseDate" format:"date-time"` UpdatedPolicyParameters []UpdatedPolicyTemplateParameter `json:"updatedPolicyParameters"` AffectedPolicies []PolicyStatus `json:"affectedPolicies"` @@ -183,8 +183,8 @@ type PolicyUpdate struct { } type UpdateStackPolicyTemplateStatusRequest struct { - TemplateCurrentVersion string `json:"templateCurrentVersion" example:"v1.0.1"` - TemplateTargetVerson string `json:"templateTargetVerson" example:"v1.0.3"` + TemplateCurrentVersion string `json:"templateCurrentVersion" validate:"version" example:"v1.0.1"` + TemplateTargetVerson string `json:"templateTargetVerson" validate:"version" example:"v1.0.3"` // PolicyUpdate []PolicyUpdate `json:"policyUpdate"` }