Skip to content

Commit

Permalink
fix(templates): disable use of jsonnet with /api/v2/templates/apply
Browse files Browse the repository at this point in the history
  • Loading branch information
williamhbaker committed Dec 29, 2021
1 parent 91f6e0d commit cf9d14c
Show file tree
Hide file tree
Showing 7 changed files with 512 additions and 20 deletions.
5 changes: 4 additions & 1 deletion pkger/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ templates for what will eventually come to support all influxdb
resources.
The parser supports JSON, Jsonnet, and YAML encodings as well as a number
of different ways to read the file/reader/string as it may.
of different ways to read the file/reader/string as it may. While the parser
supports Jsonnet, due to issues in the go-jsonnet implementation, only trusted
input should be given to the parser and therefore the EnableJsonnet() option
must be used with Parse() to enable Jsonnet support.
As an example, you can use the following to parse and validate a YAML
file and see a summary of its contents:
Expand Down
35 changes: 35 additions & 0 deletions pkger/http_server_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"

Expand Down Expand Up @@ -332,6 +333,40 @@ func (s *HTTPServerTemplates) apply(w http.ResponseWriter, r *http.Request) {
return
}

// Reject use of server-side jsonnet with /api/v2/templates/apply
if encoding == EncodingJsonnet {
s.api.Err(w, r, &errors.Error{
Code: errors.EUnprocessableEntity,
Msg: fmt.Sprintf("template from source(s) had an issue: %s", ErrInvalidEncoding.Error()),
})
return
}

var remotes []string
for _, rem := range reqBody.Remotes {
remotes = append(remotes, rem.URL)
}
remotes = append(remotes, reqBody.RawTemplate.Sources...)

for _, rem := range remotes {
// While things like '.%6Aonnet' evaluate to the default encoding (yaml), let's unescape and catch those too
decoded, err := url.QueryUnescape(rem)
if err != nil {
s.api.Err(w, r, &errors.Error{
Code: errors.EInvalid,
Msg: fmt.Sprintf("template from url[%q] had an issue", rem),
})
return
}
if len(decoded) > 0 && strings.HasSuffix(strings.ToLower(decoded), "jsonnet") {
s.api.Err(w, r, &errors.Error{
Code: errors.EUnprocessableEntity,
Msg: fmt.Sprintf("template from url[%q] had an issue: %s", rem, ErrInvalidEncoding.Error()),
})
return
}
}

var stackID platform.ID
if reqBody.StackID != nil {
if err := stackID.DecodeFromString(*reqBody.StackID); err != nil {
Expand Down
220 changes: 207 additions & 13 deletions pkger/http_server_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import (
"github.com/influxdata/influxdb/v2"
pcontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/kit/platform"
influxerror "github.com/influxdata/influxdb/v2/kit/platform/errors"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/pkg/testttp"
"github.com/influxdata/influxdb/v2/pkger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -106,6 +108,138 @@ func TestPkgerHTTPServerTemplate(t *testing.T) {
})

t.Run("dry run pkg", func(t *testing.T) {
t.Run("jsonnet disabled", func(t *testing.T) {
tests := []struct {
name string
contentType string
reqBody pkger.ReqApply
}{
{
name: "app jsonnet disabled",
contentType: "application/x-jsonnet",
reqBody: pkger.ReqApply{
DryRun: true,
OrgID: platform.ID(9000).String(),
RawTemplate: bucketPkgKinds(t, pkger.EncodingJsonnet),
},
},
{
name: "retrieves package from a URL (jsonnet disabled)",
reqBody: pkger.ReqApply{
DryRun: true,
OrgID: platform.ID(9000).String(),
Remotes: []pkger.ReqTemplateRemote{{
URL: newPkgURL(t, filesvr.URL, "testdata/bucket_associates_labels_one.jsonnet"),
}},
},
},
{
name: "app json with jsonnet disabled remote",
contentType: "application/json",
reqBody: pkger.ReqApply{
DryRun: true,
OrgID: platform.ID(9000).String(),
RawTemplate: bucketPkgJsonWithJsonnetRemote(t),
},
},
}

for _, tt := range tests {
fn := func(t *testing.T) {
svc := &fakeSVC{
dryRunFn: func(ctx context.Context, orgID, userID platform.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) {
var opt pkger.ApplyOpt
for _, o := range opts {
o(&opt)
}
pkg, err := pkger.Combine(opt.Templates)
if err != nil {
return pkger.ImpactSummary{}, err
}

if err := pkg.Validate(); err != nil {
return pkger.ImpactSummary{}, err
}
sum := pkg.Summary()
var diff pkger.Diff
for _, b := range sum.Buckets {
diff.Buckets = append(diff.Buckets, pkger.DiffBucket{
DiffIdentifier: pkger.DiffIdentifier{
MetaName: b.Name,
},
})
}
return pkger.ImpactSummary{
Summary: sum,
Diff: diff,
}, nil
},
}

core, sink := observer.New(zap.InfoLevel)
pkgHandler := pkger.NewHTTPServerTemplates(zap.New(core), svc)
svr := newMountedHandler(pkgHandler, 1)

ctx := context.Background()
testttp.
PostJSON(t, "/api/v2/templates/apply", tt.reqBody).
Headers("Content-Type", tt.contentType).
WithCtx(ctx).
Do(svr).
ExpectStatus(http.StatusUnprocessableEntity).
ExpectBody(func(buf *bytes.Buffer) {
var resp pkger.RespApply
decodeBody(t, buf, &resp)

assert.Len(t, resp.Summary.Buckets, 0)
assert.Len(t, resp.Diff.Buckets, 0)
})

// Verify logging when jsonnet is disabled
entries := sink.TakeAll() // resets to 0
if tt.contentType == "application/x-jsonnet" {
require.Equal(t, 1, len(entries))
// message 0
require.Equal(t, zap.ErrorLevel, entries[0].Entry.Level)
require.Equal(t, "api error encountered", entries[0].Entry.Message)
assert.ElementsMatch(t, []zap.Field{
zap.Error(&influxerror.Error{
Code: influxerror.EUnprocessableEntity,
Msg: "template from source(s) had an issue: invalid encoding provided",
},
)}, entries[0].Context)
} else if len(tt.reqBody.Remotes) == 1 && strings.HasSuffix(tt.reqBody.Remotes[0].URL, "jsonnet") {
require.Equal(t, 1, len(entries))
// message 0
require.Equal(t, zap.ErrorLevel, entries[0].Entry.Level)
require.Equal(t, "api error encountered", entries[0].Entry.Message)
expMsg := fmt.Sprintf("template from url[\"%s\"] had an issue: invalid encoding provided", tt.reqBody.Remotes[0].URL)
assert.ElementsMatch(t, []zap.Field{
zap.Error(&influxerror.Error{
Code: influxerror.EUnprocessableEntity,
Msg: expMsg,
},
)}, entries[0].Context)
} else if len(tt.reqBody.RawTemplate.Sources) == 1 && strings.HasSuffix(tt.reqBody.RawTemplate.Sources[0], "jsonnet") {
require.Equal(t, 1, len(entries))
// message 0
require.Equal(t, zap.ErrorLevel, entries[0].Entry.Level)
require.Equal(t, "api error encountered", entries[0].Entry.Message)
expMsg := fmt.Sprintf("template from url[\"%s\"] had an issue: invalid encoding provided", tt.reqBody.RawTemplate.Sources[0])
assert.ElementsMatch(t, []zap.Field{
zap.Error(&influxerror.Error{
Code: influxerror.EUnprocessableEntity,
Msg: expMsg,
},
)}, entries[0].Context)
} else {
require.Equal(t, 0, len(entries))
}
}
t.Run(tt.name, fn)
}
})

t.Run("json", func(t *testing.T) {
tests := []struct {
name string
Expand All @@ -130,7 +264,7 @@ func TestPkgerHTTPServerTemplate(t *testing.T) {
},
},
{
name: "retrieves package from a URL",
name: "retrieves package from a URL (json)",
reqBody: pkger.ReqApply{
DryRun: true,
OrgID: platform.ID(9000).String(),
Expand All @@ -139,15 +273,6 @@ func TestPkgerHTTPServerTemplate(t *testing.T) {
}},
},
},
{
name: "app jsonnet",
contentType: "application/x-jsonnet",
reqBody: pkger.ReqApply{
DryRun: true,
OrgID: platform.ID(9000).String(),
RawTemplate: bucketPkgKinds(t, pkger.EncodingJsonnet),
},
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -182,7 +307,8 @@ func TestPkgerHTTPServerTemplate(t *testing.T) {
},
}

pkgHandler := pkger.NewHTTPServerTemplates(zap.NewNop(), svc)
core, _ := observer.New(zap.InfoLevel)
pkgHandler := pkger.NewHTTPServerTemplates(zap.New(core), svc)
svr := newMountedHandler(pkgHandler, 1)

testttp.
Expand All @@ -198,7 +324,6 @@ func TestPkgerHTTPServerTemplate(t *testing.T) {
assert.Len(t, resp.Diff.Buckets, 1)
})
}

t.Run(tt.name, fn)
}
})
Expand Down Expand Up @@ -566,6 +691,48 @@ func TestPkgerHTTPServerTemplate(t *testing.T) {
})
})
})

t.Run("Templates()", func(t *testing.T) {
tests := []struct {
name string
reqBody pkger.ReqApply
encoding pkger.Encoding
}{
{
name: "jsonnet disabled",
reqBody: pkger.ReqApply{
OrgID: platform.ID(9000).String(),
RawTemplate: bucketPkgKinds(t, pkger.EncodingJsonnet),
},
encoding: pkger.EncodingJsonnet,
},
{
name: "jsonnet remote disabled",
reqBody: pkger.ReqApply{
OrgID: platform.ID(9000).String(),
Remotes: []pkger.ReqTemplateRemote{{
URL: newPkgURL(t, filesvr.URL, "testdata/bucket_associates_labels_one.jsonnet"),
}},
},
encoding: pkger.EncodingJsonnet,
},
{
name: "jsonnet disabled remote source",
reqBody: pkger.ReqApply{
OrgID: platform.ID(9000).String(),
RawTemplate: bucketPkgJsonWithJsonnetRemote(t),
},
encoding: pkger.EncodingJSON,
},
}

for _, tt := range tests {
tmpl, err := tt.reqBody.Templates(tt.encoding)
assert.Nil(t, tmpl)
require.Error(t, err)
assert.Equal(t, "unprocessable entity", influxerror.ErrorCode(err))
}
})
}

func assertNonZeroApplyResp(t *testing.T, resp pkger.RespApply) {
Expand Down Expand Up @@ -644,7 +811,7 @@ spec:
require.FailNow(t, "invalid encoding provided: "+encoding.String())
}

pkg, err := pkger.Parse(encoding, pkger.FromString(fmt.Sprintf(pkgStr, pkger.APIVersion)))
pkg, err := pkger.Parse(encoding, pkger.FromString(fmt.Sprintf(pkgStr, pkger.APIVersion)), pkger.EnableJsonnet())
require.NoError(t, err)

b, err := pkg.Encode(encoding)
Expand All @@ -656,6 +823,33 @@ spec:
}
}

func bucketPkgJsonWithJsonnetRemote(t *testing.T) pkger.ReqRawTemplate {
pkgStr := `[
{
"apiVersion": "%[1]s",
"kind": "Bucket",
"metadata": {
"name": "rucket-11"
},
"spec": {
"description": "bucket 1 description"
}
}
]
`
// Create a json template and then add a jsonnet remote raw template
pkg, err := pkger.Parse(pkger.EncodingJSON, pkger.FromString(fmt.Sprintf(pkgStr, pkger.APIVersion)))
require.NoError(t, err)

b, err := pkg.Encode(pkger.EncodingJSON)
require.NoError(t, err)
return pkger.ReqRawTemplate{
ContentType: pkger.EncodingJsonnet.String(),
Sources: []string{"file:///nonexistent.jsonnet"},
Template: b,
}
}

func newReqApplyYMLBody(t *testing.T, orgID platform.ID, dryRun bool) *bytes.Buffer {
t.Helper()

Expand Down
23 changes: 20 additions & 3 deletions pkger/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,16 @@ func parseJSON(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
}

func parseJsonnet(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
return parse(jsonnet.NewDecoder(r), opts...)
opt := &validateOpt{}
for _, o := range opts {
o(opt)
}
// For security, we'll default to disabling parsing jsonnet but allow callers to override the behavior via
// EnableJsonnet(). Enabling jsonnet might be useful for client code where parsing jsonnet could be acceptable.
if opt.enableJsonnet {
return parse(jsonnet.NewDecoder(r), opts...)
}
return nil, fmt.Errorf("%s: jsonnet", ErrInvalidEncoding)
}

func parseSource(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
Expand Down Expand Up @@ -536,14 +545,22 @@ func Combine(pkgs []*Template, validationOpts ...ValidateOptFn) (*Template, erro

type (
validateOpt struct {
minResources bool
skipValidate bool
minResources bool
skipValidate bool
enableJsonnet bool
}

// ValidateOptFn provides a means to disable desired validation checks.
ValidateOptFn func(*validateOpt)
)

// Jsonnet parsing is disabled by default. EnableJsonnet turns it back on.
func EnableJsonnet() ValidateOptFn {
return func(opt *validateOpt) {
opt.enableJsonnet = true
}
}

// ValidWithoutResources ignores the validation check for minimum number
// of resources. This is useful for the service Create to ignore this and
// allow the creation of a pkg without resources.
Expand Down
Loading

0 comments on commit cf9d14c

Please sign in to comment.