Skip to content

Commit

Permalink
Add descriptions and missing validation for service-intentions (#385)
Browse files Browse the repository at this point in the history
* Add descriptions and missing validation for service-intentions

Co-authored-by: Iryna Shustava <ishustava@users.noreply.github.com>
Co-authored-by: Luke Kysow <lkysow@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 9, 2020
1 parent 5854bff commit 548ae2e
Show file tree
Hide file tree
Showing 3 changed files with 349 additions and 45 deletions.
153 changes: 127 additions & 26 deletions api/v1alpha1/serviceintentions_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v1alpha1

import (
"encoding/json"
"net/http"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
Expand All @@ -18,12 +19,22 @@ import (

// ServiceIntentionsSpec defines the desired state of ServiceIntentions
type ServiceIntentionsSpec struct {
Destination Destination `json:"destination,omitempty"`
Sources SourceIntentions `json:"sources,omitempty"`
// Destination is the intention destination that will have the authorization granted to.
Destination Destination `json:"destination,omitempty"`
// Sources is the list of all intention sources and the authorization granted to those sources.
// The order of this list does not matter, but out of convenience Consul will always store this
// reverse sorted by intention precedence, as that is the order that they will be evaluated at enforcement time.
Sources SourceIntentions `json:"sources,omitempty"`
}

type Destination struct {
Name string `json:"name,omitempty"`
// Name is the destination of all intentions defined in this config entry.
// This may be set to the wildcard character (*) to match
// all services that don't otherwise have intentions defined.
Name string `json:"name,omitempty"`
// Namespace specifies the namespace the config entry will apply to.
// This may be set to the wildcard character (*) to match all services
// in all namespaces that don't otherwise have intentions defined.
Namespace string `json:"namespace,omitempty"`
}

Expand All @@ -32,36 +43,63 @@ type IntentionPermissions []*IntentionPermission
type IntentionHTTPHeaderPermissions []IntentionHTTPHeaderPermission

type SourceIntention struct {
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
Action IntentionAction `json:"action,omitempty"`
// Name is the source of the intention. This is the name of a
// Consul service. The service doesn't need to be registered.
Name string `json:"name,omitempty"`
// Namespace is the namespace for the Name parameter.
Namespace string `json:"namespace,omitempty"`
// Action is required for an L4 intention, and should be set to one of
// "allow" or "deny" for the action that should be taken if this intention matches a request.
Action IntentionAction `json:"action,omitempty"`
// Permissions is the list of all additional L7 attributes that extend the intention match criteria.
// Permission precedence is applied top to bottom. For any given request the first permission to match
// in the list is terminal and stops further evaluation. As with L4 intentions, traffic that fails to
// match any of the provided permissions in this intention will be subject to the default intention
// behavior is defined by the default ACL policy. This should be omitted for an L4 intention
// as it is mutually exclusive with the Action field.
Permissions IntentionPermissions `json:"permissions,omitempty"`
Description string `json:"description,omitempty"`
// Description for the intention. This is not used by Consul, but is presented in API responses to assist tooling.
Description string `json:"description,omitempty"`
}

type IntentionPermission struct {
Action IntentionAction `json:"action,omitempty"`
HTTP *IntentionHTTPPermission `json:"http,omitempty"`
// Action is one of "allow" or "deny" for the action that
// should be taken if this permission matches a request.
Action IntentionAction `json:"action,omitempty"`
// HTTP is a set of HTTP-specific authorization criteria.
HTTP *IntentionHTTPPermission `json:"http,omitempty"`
}

type IntentionHTTPPermission struct {
PathExact string `json:"pathExact,omitempty"`
// PathExact is the exact path to match on the HTTP request path.
PathExact string `json:"pathExact,omitempty"`
// PathPrefix is the path prefix to match on the HTTP request path.
PathPrefix string `json:"pathPrefix,omitempty"`
PathRegex string `json:"pathRegex,omitempty"`

// PathRegex is the regular expression to match on the HTTP request path.
PathRegex string `json:"pathRegex,omitempty"`
// Header is a set of criteria that can match on HTTP request headers.
// If more than one is configured all must match for the overall match to apply.
Header IntentionHTTPHeaderPermissions `json:"header,omitempty"`

// Methods is a list of HTTP methods for which this match applies. If unspecified
// all HTTP methods are matched. If provided the names must be a valid method.
Methods []string `json:"methods,omitempty"`
}

type IntentionHTTPHeaderPermission struct {
Name string `json:"name,omitempty"`
Present bool `json:"present,omitempty"`
Exact string `json:"exact,omitempty"`
Prefix string `json:"prefix,omitempty"`
Suffix string `json:"suffix,omitempty"`
Regex string `json:"regex,omitempty"`
Invert bool `json:"invert,omitempty"`
// Name is the name of the header to match.
Name string `json:"name,omitempty"`
// Present matches if the header with the given name is present with any value.
Present bool `json:"present,omitempty"`
// Exact matches if the header with the given name is this value.
Exact string `json:"exact,omitempty"`
// Prefix matches if the header with the given name has this prefix.
Prefix string `json:"prefix,omitempty"`
// Suffix matches if the header with the given name has this suffix.
Suffix string `json:"suffix,omitempty"`
// Regex matches if the header with the given name matches this pattern.
Regex string `json:"regex,omitempty"`
// Invert inverts the logic of the match.
Invert bool `json:"invert,omitempty"`
}

// IntentionAction is the action that the intention represents. This
Expand Down Expand Up @@ -196,13 +234,13 @@ func (in IntentionPermissions) toConsul() []*capi.IntentionPermission {
for _, permission := range in {
consulIntentionPermissions = append(consulIntentionPermissions, &capi.IntentionPermission{
Action: permission.Action.toConsul(),
HTTP: permission.HTTP.ToConsul(),
HTTP: permission.HTTP.toConsul(),
})
}
return consulIntentionPermissions
}

func (in *IntentionHTTPPermission) ToConsul() *capi.IntentionHTTPPermission {
func (in *IntentionHTTPPermission) toConsul() *capi.IntentionHTTPPermission {
if in == nil {
return nil
}
Expand Down Expand Up @@ -296,11 +334,64 @@ func (in IntentionPermissions) validate(path *field.Path) field.ErrorList {

func (in *IntentionHTTPPermission) validate(path *field.Path) field.ErrorList {
var errs field.ErrorList
if invalidPathPrefix(in.PathPrefix) {
errs = append(errs, field.Invalid(path.Child("pathPrefix"), in.PathPrefix, `must begin with a '/'`))
pathParts := 0
if in.PathRegex != "" {
pathParts++
}
if in.PathPrefix != "" {
pathParts++
if invalidPathPrefix(in.PathPrefix) {
errs = append(errs, field.Invalid(path.Child("pathPrefix"), in.PathPrefix, `must begin with a '/'`))
}
}
if in.PathExact != "" {
pathParts++
if invalidPathPrefix(in.PathExact) {
errs = append(errs, field.Invalid(path.Child("pathExact"), in.PathExact, `must begin with a '/'`))
}
}
if pathParts > 1 {
asJSON, _ := json.Marshal(in)
errs = append(errs, field.Invalid(path, string(asJSON), `at most only one of pathExact, pathPrefix, or pathRegex may be configured.`))
}

found := make(map[string]struct{})
for i, method := range in.Methods {
methods := []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodConnect,
http.MethodOptions,
http.MethodTrace,
}
if !sliceContains(methods, method) {
errs = append(errs, field.Invalid(path.Child("methods").Index(i), method, notInSliceMessage(methods)))
}
if _, ok := found[method]; ok {
errs = append(errs, field.Invalid(path.Child("methods").Index(i), method, `method listed more than once.`))
}
found[method] = struct{}{}
}
if invalidPathPrefix(in.PathExact) {
errs = append(errs, field.Invalid(path.Child("pathExact"), in.PathExact, `must begin with a '/'`))
errs = append(errs, in.Header.validate(path.Child("header"))...)
return errs
}

func (in IntentionHTTPHeaderPermissions) validate(path *field.Path) field.ErrorList {
var errs field.ErrorList
for i, permission := range in {
hdrParts := 0
if permission.Present {
hdrParts++
}
hdrParts += numNotEmpty(permission.Exact, permission.Regex, permission.Prefix, permission.Suffix)
if hdrParts > 1 {
asJson, _ := json.Marshal(in[i])
errs = append(errs, field.Invalid(path.Index(i), string(asJson), `at most only one of exact, prefix, suffix, regex, or present may be configured.`))
}
}
return errs
}
Expand Down Expand Up @@ -355,3 +446,13 @@ type ServiceIntentionsList struct {
func init() {
SchemeBuilder.Register(&ServiceIntentions{}, &ServiceIntentionsList{})
}

func numNotEmpty(ss ...string) int {
count := 0
for _, s := range ss {
if s != "" {
count++
}
}
return count
}
Loading

0 comments on commit 548ae2e

Please sign in to comment.