2020-06-29 18:16:55 +00:00
|
|
|
package pkger
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2021-12-30 17:55:45 +00:00
|
|
|
"net/url"
|
2020-06-29 18:16:55 +00:00
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/go-chi/chi"
|
|
|
|
"github.com/go-chi/chi/middleware"
|
|
|
|
pctx "github.com/influxdata/influxdb/v2/context"
|
|
|
|
ierrors "github.com/influxdata/influxdb/v2/kit/errors"
|
2021-09-13 19:12:35 +00:00
|
|
|
"github.com/influxdata/influxdb/v2/kit/platform"
|
|
|
|
"github.com/influxdata/influxdb/v2/kit/platform/errors"
|
2020-06-29 18:16:55 +00:00
|
|
|
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
|
|
|
"github.com/influxdata/influxdb/v2/pkg/jsonnet"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
const RoutePrefixTemplates = "/api/v2/templates"
|
|
|
|
|
|
|
|
// HTTPServerTemplates is a server that manages the templates HTTP transport.
|
|
|
|
type HTTPServerTemplates struct {
|
|
|
|
chi.Router
|
|
|
|
api *kithttp.API
|
|
|
|
logger *zap.Logger
|
|
|
|
svc SVC
|
feat: add --hardening-enabled option to limit flux/pkger HTTP requests (#23207)
Flux HTTP and template fetching requests do not perform IP address
checks for local addresses. This behavior on the one hand allows SSRF
(Server Side Request Forgery) attacks via authenticated requests but on
the other hand is useful for scenarios that have legitimate requirements
to fetch from private addresses (eg, hosting templates internally or
performing flux queries to local resources during development).
To not break existing installations, the default behavior will remain
the same but a new --hardening-enabled option is added to influxd to
turn on IP address verification and limit both flux and template
fetching HTTP requests to non-private addresses. We plan to enable new
security features that aren't suitable for the default install with this
option. Put another way, this new option is intended to be used to make
it easy to turn on all security options when running in production
environments. The 'Manage security and authorization' section of the
docs will also be updated for this option.
Specifically for flux, when --hardening-enabled is specified, we now
pass in PrivateIPValidator{} to the flux dependency configuration. The
flux url validator will then tap into the http.Client 'Control'
mechanism to validate the IP address since it is called after DNS lookup
but before the connection starts.
For pkger (template fetching), when --hardening-enabled is specified,
the template parser's HTTP client will be configured to also use
PrivateIPValidator{}. Note that /api/v2/stacks POST ('init', aka create)
and PATCH ('update') only store the new url to be applied later with
/api/v2/templates/apply. While it is possible to have InitStack() and
UpdateStack() mimic net.DialContext() to setup a go routine to perform a
DNS lookup and then loop through the returned addresses to verify none
are for a private IP before storing the url, this would add considerable
complexity to the stacks implementation. Since the stack's urls are
fetched when it is applied and the IP address is verified as part of
apply (see above), for now we'll keep this simple and not validate the
IPs of the stack's urls during init or update.
Lastly, update pkger/http_server_template_test.go's Templates() test for
disabled jsonnet to also check the contents of the 422 error (since the
flux validator also returns a 422 with different message). Also, fix the
URL in one of these tests to use a valid path.
2022-03-18 14:25:31 +00:00
|
|
|
client *http.Client
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewHTTPServerTemplates constructs a new http server.
|
feat: add --hardening-enabled option to limit flux/pkger HTTP requests (#23207)
Flux HTTP and template fetching requests do not perform IP address
checks for local addresses. This behavior on the one hand allows SSRF
(Server Side Request Forgery) attacks via authenticated requests but on
the other hand is useful for scenarios that have legitimate requirements
to fetch from private addresses (eg, hosting templates internally or
performing flux queries to local resources during development).
To not break existing installations, the default behavior will remain
the same but a new --hardening-enabled option is added to influxd to
turn on IP address verification and limit both flux and template
fetching HTTP requests to non-private addresses. We plan to enable new
security features that aren't suitable for the default install with this
option. Put another way, this new option is intended to be used to make
it easy to turn on all security options when running in production
environments. The 'Manage security and authorization' section of the
docs will also be updated for this option.
Specifically for flux, when --hardening-enabled is specified, we now
pass in PrivateIPValidator{} to the flux dependency configuration. The
flux url validator will then tap into the http.Client 'Control'
mechanism to validate the IP address since it is called after DNS lookup
but before the connection starts.
For pkger (template fetching), when --hardening-enabled is specified,
the template parser's HTTP client will be configured to also use
PrivateIPValidator{}. Note that /api/v2/stacks POST ('init', aka create)
and PATCH ('update') only store the new url to be applied later with
/api/v2/templates/apply. While it is possible to have InitStack() and
UpdateStack() mimic net.DialContext() to setup a go routine to perform a
DNS lookup and then loop through the returned addresses to verify none
are for a private IP before storing the url, this would add considerable
complexity to the stacks implementation. Since the stack's urls are
fetched when it is applied and the IP address is verified as part of
apply (see above), for now we'll keep this simple and not validate the
IPs of the stack's urls during init or update.
Lastly, update pkger/http_server_template_test.go's Templates() test for
disabled jsonnet to also check the contents of the 422 error (since the
flux validator also returns a 422 with different message). Also, fix the
URL in one of these tests to use a valid path.
2022-03-18 14:25:31 +00:00
|
|
|
func NewHTTPServerTemplates(log *zap.Logger, svc SVC, client *http.Client) *HTTPServerTemplates {
|
2020-06-29 18:16:55 +00:00
|
|
|
svr := &HTTPServerTemplates{
|
|
|
|
api: kithttp.NewAPI(kithttp.WithLog(log)),
|
|
|
|
logger: log,
|
|
|
|
svc: svc,
|
feat: add --hardening-enabled option to limit flux/pkger HTTP requests (#23207)
Flux HTTP and template fetching requests do not perform IP address
checks for local addresses. This behavior on the one hand allows SSRF
(Server Side Request Forgery) attacks via authenticated requests but on
the other hand is useful for scenarios that have legitimate requirements
to fetch from private addresses (eg, hosting templates internally or
performing flux queries to local resources during development).
To not break existing installations, the default behavior will remain
the same but a new --hardening-enabled option is added to influxd to
turn on IP address verification and limit both flux and template
fetching HTTP requests to non-private addresses. We plan to enable new
security features that aren't suitable for the default install with this
option. Put another way, this new option is intended to be used to make
it easy to turn on all security options when running in production
environments. The 'Manage security and authorization' section of the
docs will also be updated for this option.
Specifically for flux, when --hardening-enabled is specified, we now
pass in PrivateIPValidator{} to the flux dependency configuration. The
flux url validator will then tap into the http.Client 'Control'
mechanism to validate the IP address since it is called after DNS lookup
but before the connection starts.
For pkger (template fetching), when --hardening-enabled is specified,
the template parser's HTTP client will be configured to also use
PrivateIPValidator{}. Note that /api/v2/stacks POST ('init', aka create)
and PATCH ('update') only store the new url to be applied later with
/api/v2/templates/apply. While it is possible to have InitStack() and
UpdateStack() mimic net.DialContext() to setup a go routine to perform a
DNS lookup and then loop through the returned addresses to verify none
are for a private IP before storing the url, this would add considerable
complexity to the stacks implementation. Since the stack's urls are
fetched when it is applied and the IP address is verified as part of
apply (see above), for now we'll keep this simple and not validate the
IPs of the stack's urls during init or update.
Lastly, update pkger/http_server_template_test.go's Templates() test for
disabled jsonnet to also check the contents of the 422 error (since the
flux validator also returns a 422 with different message). Also, fix the
URL in one of these tests to use a valid path.
2022-03-18 14:25:31 +00:00
|
|
|
client: client,
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
exportAllowContentTypes := middleware.AllowContentType("text/yml", "application/x-yaml", "application/json")
|
|
|
|
setJSONContentType := middleware.SetHeader("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
|
|
|
r := chi.NewRouter()
|
|
|
|
{
|
|
|
|
r.With(exportAllowContentTypes).Post("/export", svr.export)
|
|
|
|
r.With(setJSONContentType).Post("/apply", svr.apply)
|
|
|
|
}
|
|
|
|
|
|
|
|
svr.Router = r
|
|
|
|
return svr
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prefix provides the prefix to this route tree.
|
|
|
|
func (s *HTTPServerTemplates) Prefix() string {
|
|
|
|
return RoutePrefixTemplates
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReqExportOrgIDOpt provides options to export resources by organization id.
|
|
|
|
type ReqExportOrgIDOpt struct {
|
|
|
|
OrgID string `json:"orgID"`
|
|
|
|
Filters struct {
|
|
|
|
ByLabel []string `json:"byLabel"`
|
|
|
|
ByResourceKind []Kind `json:"byResourceKind"`
|
|
|
|
} `json:"resourceFilters"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReqExport is a request body for the export endpoint.
|
|
|
|
type ReqExport struct {
|
|
|
|
StackID string `json:"stackID"`
|
|
|
|
OrgIDs []ReqExportOrgIDOpt `json:"orgIDs"`
|
|
|
|
Resources []ResourceToClone `json:"resources"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// OK validates a create request.
|
|
|
|
func (r *ReqExport) OK() error {
|
|
|
|
if len(r.Resources) == 0 && len(r.OrgIDs) == 0 && r.StackID == "" {
|
2021-03-30 18:10:02 +00:00
|
|
|
return &errors.Error{
|
|
|
|
Code: errors.EUnprocessableEntity,
|
2020-06-29 18:16:55 +00:00
|
|
|
Msg: "at least 1 resource, 1 org id, or stack id must be provided",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, org := range r.OrgIDs {
|
2021-03-30 18:10:02 +00:00
|
|
|
if _, err := platform.IDFromString(org.OrgID); err != nil {
|
|
|
|
return &errors.Error{
|
|
|
|
Code: errors.EInvalid,
|
2020-06-29 18:16:55 +00:00
|
|
|
Msg: fmt.Sprintf("provided org id is invalid: %q", org.OrgID),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.StackID != "" {
|
2021-03-30 18:10:02 +00:00
|
|
|
_, err := platform.IDFromString(r.StackID)
|
2020-06-29 18:16:55 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
// RespExport is a response body for the create template endpoint.
|
2020-06-29 18:16:55 +00:00
|
|
|
type RespExport []Object
|
|
|
|
|
|
|
|
func (s *HTTPServerTemplates) export(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var reqBody ReqExport
|
|
|
|
if err := s.api.DecodeJSON(r.Body, &reqBody); err != nil {
|
|
|
|
s.api.Err(w, r, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
|
|
|
|
opts := []ExportOptFn{
|
|
|
|
ExportWithExistingResources(reqBody.Resources...),
|
|
|
|
}
|
|
|
|
for _, orgIDStr := range reqBody.OrgIDs {
|
2021-03-30 18:10:02 +00:00
|
|
|
orgID, err := platform.IDFromString(orgIDStr.OrgID)
|
2020-06-29 18:16:55 +00:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
opts = append(opts, ExportWithAllOrgResources(ExportByOrgIDOpt{
|
|
|
|
OrgID: *orgID,
|
|
|
|
LabelNames: orgIDStr.Filters.ByLabel,
|
|
|
|
ResourceKinds: orgIDStr.Filters.ByResourceKind,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
if reqBody.StackID != "" {
|
2021-03-30 18:10:02 +00:00
|
|
|
stackID, err := platform.IDFromString(reqBody.StackID)
|
2020-06-29 18:16:55 +00:00
|
|
|
if err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
s.api.Err(w, r, &errors.Error{
|
|
|
|
Code: errors.EInvalid,
|
2020-06-29 18:16:55 +00:00
|
|
|
Msg: fmt.Sprintf("invalid stack ID provided: %q", reqBody.StackID),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
opts = append(opts, ExportWithStackID(*stackID))
|
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
newTemplate, err := s.svc.Export(r.Context(), opts...)
|
2020-06-29 18:16:55 +00:00
|
|
|
if err != nil {
|
|
|
|
s.api.Err(w, r, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
resp := RespExport(newTemplate.Objects)
|
2020-06-29 18:16:55 +00:00
|
|
|
if resp == nil {
|
|
|
|
resp = []Object{}
|
|
|
|
}
|
|
|
|
|
|
|
|
var enc encoder
|
2020-06-30 21:54:00 +00:00
|
|
|
switch templateEncoding(r.Header.Get("Accept")) {
|
2020-06-29 18:16:55 +00:00
|
|
|
case EncodingYAML:
|
|
|
|
enc = yaml.NewEncoder(w)
|
|
|
|
w.Header().Set("Content-Type", "application/x-yaml")
|
|
|
|
default:
|
|
|
|
enc = newJSONEnc(w)
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
}
|
|
|
|
|
|
|
|
s.encResp(w, r, enc, http.StatusOK, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReqTemplateRemote provides a package via a remote (i.e. a gist). If content type is not
|
|
|
|
// provided then the service will do its best to discern the content type of the
|
|
|
|
// contents.
|
|
|
|
type ReqTemplateRemote struct {
|
|
|
|
URL string `json:"url" yaml:"url"`
|
|
|
|
ContentType string `json:"contentType" yaml:"contentType"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encoding returns the encoding type that corresponds to the given content type.
|
|
|
|
func (p ReqTemplateRemote) Encoding() Encoding {
|
|
|
|
return convertEncoding(p.ContentType, p.URL)
|
|
|
|
}
|
|
|
|
|
|
|
|
type ReqRawTemplate struct {
|
|
|
|
ContentType string `json:"contentType" yaml:"contentType"`
|
|
|
|
Sources []string `json:"sources" yaml:"sources"`
|
2020-06-30 21:54:00 +00:00
|
|
|
Template json.RawMessage `json:"contents" yaml:"contents"`
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p ReqRawTemplate) Encoding() Encoding {
|
|
|
|
var source string
|
|
|
|
if len(p.Sources) > 0 {
|
|
|
|
source = p.Sources[0]
|
|
|
|
}
|
|
|
|
return convertEncoding(p.ContentType, source)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReqRawAction is a raw action consumers can provide to change the behavior
|
|
|
|
// of the application of a template.
|
|
|
|
type ReqRawAction struct {
|
|
|
|
Action string `json:"action"`
|
|
|
|
Properties json.RawMessage `json:"properties"`
|
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
// ReqApply is the request body for a json or yaml body for the apply template endpoint.
|
2020-06-29 18:16:55 +00:00
|
|
|
type ReqApply struct {
|
|
|
|
DryRun bool `json:"dryRun" yaml:"dryRun"`
|
|
|
|
OrgID string `json:"orgID" yaml:"orgID"`
|
|
|
|
StackID *string `json:"stackID" yaml:"stackID"` // optional: non nil value signals stack should be used
|
|
|
|
Remotes []ReqTemplateRemote `json:"remotes" yaml:"remotes"`
|
|
|
|
|
|
|
|
RawTemplates []ReqRawTemplate `json:"templates" yaml:"templates"`
|
|
|
|
RawTemplate ReqRawTemplate `json:"template" yaml:"template"`
|
|
|
|
|
2020-07-28 18:27:52 +00:00
|
|
|
EnvRefs map[string]interface{} `json:"envRefs"`
|
|
|
|
Secrets map[string]string `json:"secrets"`
|
2020-06-29 18:16:55 +00:00
|
|
|
|
|
|
|
RawActions []ReqRawAction `json:"actions"`
|
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
// Templates returns all templates associated with the request.
|
feat: add --hardening-enabled option to limit flux/pkger HTTP requests (#23207)
Flux HTTP and template fetching requests do not perform IP address
checks for local addresses. This behavior on the one hand allows SSRF
(Server Side Request Forgery) attacks via authenticated requests but on
the other hand is useful for scenarios that have legitimate requirements
to fetch from private addresses (eg, hosting templates internally or
performing flux queries to local resources during development).
To not break existing installations, the default behavior will remain
the same but a new --hardening-enabled option is added to influxd to
turn on IP address verification and limit both flux and template
fetching HTTP requests to non-private addresses. We plan to enable new
security features that aren't suitable for the default install with this
option. Put another way, this new option is intended to be used to make
it easy to turn on all security options when running in production
environments. The 'Manage security and authorization' section of the
docs will also be updated for this option.
Specifically for flux, when --hardening-enabled is specified, we now
pass in PrivateIPValidator{} to the flux dependency configuration. The
flux url validator will then tap into the http.Client 'Control'
mechanism to validate the IP address since it is called after DNS lookup
but before the connection starts.
For pkger (template fetching), when --hardening-enabled is specified,
the template parser's HTTP client will be configured to also use
PrivateIPValidator{}. Note that /api/v2/stacks POST ('init', aka create)
and PATCH ('update') only store the new url to be applied later with
/api/v2/templates/apply. While it is possible to have InitStack() and
UpdateStack() mimic net.DialContext() to setup a go routine to perform a
DNS lookup and then loop through the returned addresses to verify none
are for a private IP before storing the url, this would add considerable
complexity to the stacks implementation. Since the stack's urls are
fetched when it is applied and the IP address is verified as part of
apply (see above), for now we'll keep this simple and not validate the
IPs of the stack's urls during init or update.
Lastly, update pkger/http_server_template_test.go's Templates() test for
disabled jsonnet to also check the contents of the 422 error (since the
flux validator also returns a 422 with different message). Also, fix the
URL in one of these tests to use a valid path.
2022-03-18 14:25:31 +00:00
|
|
|
func (r ReqApply) Templates(encoding Encoding, client *http.Client) (*Template, error) {
|
2020-06-30 21:54:00 +00:00
|
|
|
var rawTemplates []*Template
|
2020-06-29 18:16:55 +00:00
|
|
|
for _, rem := range r.Remotes {
|
|
|
|
if rem.URL == "" {
|
|
|
|
continue
|
|
|
|
}
|
feat: add --hardening-enabled option to limit flux/pkger HTTP requests (#23207)
Flux HTTP and template fetching requests do not perform IP address
checks for local addresses. This behavior on the one hand allows SSRF
(Server Side Request Forgery) attacks via authenticated requests but on
the other hand is useful for scenarios that have legitimate requirements
to fetch from private addresses (eg, hosting templates internally or
performing flux queries to local resources during development).
To not break existing installations, the default behavior will remain
the same but a new --hardening-enabled option is added to influxd to
turn on IP address verification and limit both flux and template
fetching HTTP requests to non-private addresses. We plan to enable new
security features that aren't suitable for the default install with this
option. Put another way, this new option is intended to be used to make
it easy to turn on all security options when running in production
environments. The 'Manage security and authorization' section of the
docs will also be updated for this option.
Specifically for flux, when --hardening-enabled is specified, we now
pass in PrivateIPValidator{} to the flux dependency configuration. The
flux url validator will then tap into the http.Client 'Control'
mechanism to validate the IP address since it is called after DNS lookup
but before the connection starts.
For pkger (template fetching), when --hardening-enabled is specified,
the template parser's HTTP client will be configured to also use
PrivateIPValidator{}. Note that /api/v2/stacks POST ('init', aka create)
and PATCH ('update') only store the new url to be applied later with
/api/v2/templates/apply. While it is possible to have InitStack() and
UpdateStack() mimic net.DialContext() to setup a go routine to perform a
DNS lookup and then loop through the returned addresses to verify none
are for a private IP before storing the url, this would add considerable
complexity to the stacks implementation. Since the stack's urls are
fetched when it is applied and the IP address is verified as part of
apply (see above), for now we'll keep this simple and not validate the
IPs of the stack's urls during init or update.
Lastly, update pkger/http_server_template_test.go's Templates() test for
disabled jsonnet to also check the contents of the 422 error (since the
flux validator also returns a 422 with different message). Also, fix the
URL in one of these tests to use a valid path.
2022-03-18 14:25:31 +00:00
|
|
|
template, err := Parse(rem.Encoding(), FromHTTPRequest(rem.URL, client), ValidSkipParseError())
|
2020-06-29 18:16:55 +00:00
|
|
|
if err != nil {
|
2020-06-30 21:54:00 +00:00
|
|
|
msg := fmt.Sprintf("template from url[%s] had an issue: %s", rem.URL, err.Error())
|
2021-03-30 18:10:02 +00:00
|
|
|
return nil, influxErr(errors.EUnprocessableEntity, msg)
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
2020-06-30 21:54:00 +00:00
|
|
|
rawTemplates = append(rawTemplates, template)
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for i, rawTmpl := range append(r.RawTemplates, r.RawTemplate) {
|
2020-06-30 21:54:00 +00:00
|
|
|
if rawTmpl.Template == nil {
|
2020-06-29 18:16:55 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
enc := encoding
|
|
|
|
if sourceEncoding := rawTmpl.Encoding(); sourceEncoding != EncodingSource {
|
|
|
|
enc = sourceEncoding
|
|
|
|
}
|
2020-06-30 21:54:00 +00:00
|
|
|
template, err := Parse(enc, FromReader(bytes.NewReader(rawTmpl.Template), rawTmpl.Sources...), ValidSkipParseError())
|
2020-06-29 18:16:55 +00:00
|
|
|
if err != nil {
|
|
|
|
sources := formatSources(rawTmpl.Sources)
|
2020-06-30 21:54:00 +00:00
|
|
|
msg := fmt.Sprintf("template[%d] from source(s) %q had an issue: %s", i, sources, err.Error())
|
2021-03-30 18:10:02 +00:00
|
|
|
return nil, influxErr(errors.EUnprocessableEntity, msg)
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
2020-06-30 21:54:00 +00:00
|
|
|
rawTemplates = append(rawTemplates, template)
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
return Combine(rawTemplates, ValidWithoutResources(), ValidSkipParseError())
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type actionType string
|
|
|
|
|
|
|
|
// various ActionTypes the transport API speaks
|
|
|
|
const (
|
|
|
|
ActionTypeSkipKind actionType = "skipKind"
|
|
|
|
ActionTypeSkipResource actionType = "skipResource"
|
|
|
|
)
|
|
|
|
|
|
|
|
func (r ReqApply) validActions() (struct {
|
|
|
|
SkipKinds []ActionSkipKind
|
|
|
|
SkipResources []ActionSkipResource
|
|
|
|
}, error) {
|
|
|
|
type actions struct {
|
|
|
|
SkipKinds []ActionSkipKind
|
|
|
|
SkipResources []ActionSkipResource
|
|
|
|
}
|
|
|
|
|
|
|
|
unmarshalErrFn := func(err error, idx int, actionType string) error {
|
|
|
|
msg := fmt.Sprintf("failed to unmarshal properties for actions[%d] %q", idx, actionType)
|
|
|
|
return ierrors.Wrap(err, msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
kindErrFn := func(err error, idx int, actionType string) error {
|
|
|
|
msg := fmt.Sprintf("invalid kind for actions[%d] %q", idx, actionType)
|
|
|
|
return ierrors.Wrap(err, msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
var out actions
|
|
|
|
for i, rawAct := range r.RawActions {
|
|
|
|
switch a := rawAct.Action; actionType(a) {
|
|
|
|
case ActionTypeSkipResource:
|
|
|
|
var asr ActionSkipResource
|
|
|
|
if err := json.Unmarshal(rawAct.Properties, &asr); err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
return actions{}, influxErr(errors.EInvalid, unmarshalErrFn(err, i, a))
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
if err := asr.Kind.OK(); err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
return actions{}, influxErr(errors.EInvalid, kindErrFn(err, i, a))
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
out.SkipResources = append(out.SkipResources, asr)
|
|
|
|
case ActionTypeSkipKind:
|
|
|
|
var ask ActionSkipKind
|
|
|
|
if err := json.Unmarshal(rawAct.Properties, &ask); err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
return actions{}, influxErr(errors.EInvalid, unmarshalErrFn(err, i, a))
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
if err := ask.Kind.OK(); err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
return actions{}, influxErr(errors.EInvalid, kindErrFn(err, i, a))
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
out.SkipKinds = append(out.SkipKinds, ask)
|
|
|
|
default:
|
|
|
|
msg := fmt.Sprintf(
|
|
|
|
"invalid action type %q provided for actions[%d] ; Must be one of [%s]",
|
|
|
|
a, i, ActionTypeSkipResource,
|
|
|
|
)
|
2021-03-30 18:10:02 +00:00
|
|
|
return actions{}, influxErr(errors.EInvalid, msg)
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
// RespApply is the response body for the apply template endpoint.
|
2020-06-29 18:16:55 +00:00
|
|
|
type RespApply struct {
|
|
|
|
Sources []string `json:"sources" yaml:"sources"`
|
|
|
|
StackID string `json:"stackID" yaml:"stackID"`
|
|
|
|
Diff Diff `json:"diff" yaml:"diff"`
|
|
|
|
Summary Summary `json:"summary" yaml:"summary"`
|
|
|
|
|
|
|
|
Errors []ValidationErr `json:"errors,omitempty" yaml:"errors,omitempty"`
|
|
|
|
}
|
|
|
|
|
2022-02-11 22:28:49 +00:00
|
|
|
// RespApplyErr is the response body for a dry-run parse error in the apply template endpoint.
|
|
|
|
type RespApplyErr struct {
|
|
|
|
RespApply
|
|
|
|
|
|
|
|
Code string `json:"code" yaml:"code"`
|
|
|
|
Message string `json:"message" yaml:"message"`
|
|
|
|
}
|
|
|
|
|
2020-06-29 18:16:55 +00:00
|
|
|
func (s *HTTPServerTemplates) apply(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var reqBody ReqApply
|
|
|
|
encoding, err := decodeWithEncoding(r, &reqBody)
|
|
|
|
if err != nil {
|
|
|
|
s.api.Err(w, r, newDecodeErr(encoding.String(), err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-30 18:10:02 +00:00
|
|
|
orgID, err := platform.IDFromString(reqBody.OrgID)
|
2020-06-29 18:16:55 +00:00
|
|
|
if err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
s.api.Err(w, r, &errors.Error{
|
|
|
|
Code: errors.EInvalid,
|
2020-06-29 18:16:55 +00:00
|
|
|
Msg: fmt.Sprintf("invalid organization ID provided: %q", reqBody.OrgID),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-12-30 17:55:45 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-30 18:10:02 +00:00
|
|
|
var stackID platform.ID
|
2020-06-29 18:16:55 +00:00
|
|
|
if reqBody.StackID != nil {
|
|
|
|
if err := stackID.DecodeFromString(*reqBody.StackID); err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
s.api.Err(w, r, &errors.Error{
|
|
|
|
Code: errors.EInvalid,
|
2020-06-29 18:16:55 +00:00
|
|
|
Msg: fmt.Sprintf("invalid stack ID provided: %q", *reqBody.StackID),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
feat: add --hardening-enabled option to limit flux/pkger HTTP requests (#23207)
Flux HTTP and template fetching requests do not perform IP address
checks for local addresses. This behavior on the one hand allows SSRF
(Server Side Request Forgery) attacks via authenticated requests but on
the other hand is useful for scenarios that have legitimate requirements
to fetch from private addresses (eg, hosting templates internally or
performing flux queries to local resources during development).
To not break existing installations, the default behavior will remain
the same but a new --hardening-enabled option is added to influxd to
turn on IP address verification and limit both flux and template
fetching HTTP requests to non-private addresses. We plan to enable new
security features that aren't suitable for the default install with this
option. Put another way, this new option is intended to be used to make
it easy to turn on all security options when running in production
environments. The 'Manage security and authorization' section of the
docs will also be updated for this option.
Specifically for flux, when --hardening-enabled is specified, we now
pass in PrivateIPValidator{} to the flux dependency configuration. The
flux url validator will then tap into the http.Client 'Control'
mechanism to validate the IP address since it is called after DNS lookup
but before the connection starts.
For pkger (template fetching), when --hardening-enabled is specified,
the template parser's HTTP client will be configured to also use
PrivateIPValidator{}. Note that /api/v2/stacks POST ('init', aka create)
and PATCH ('update') only store the new url to be applied later with
/api/v2/templates/apply. While it is possible to have InitStack() and
UpdateStack() mimic net.DialContext() to setup a go routine to perform a
DNS lookup and then loop through the returned addresses to verify none
are for a private IP before storing the url, this would add considerable
complexity to the stacks implementation. Since the stack's urls are
fetched when it is applied and the IP address is verified as part of
apply (see above), for now we'll keep this simple and not validate the
IPs of the stack's urls during init or update.
Lastly, update pkger/http_server_template_test.go's Templates() test for
disabled jsonnet to also check the contents of the 422 error (since the
flux validator also returns a 422 with different message). Also, fix the
URL in one of these tests to use a valid path.
2022-03-18 14:25:31 +00:00
|
|
|
parsedTemplate, err := reqBody.Templates(encoding, s.client)
|
2020-06-29 18:16:55 +00:00
|
|
|
if err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
s.api.Err(w, r, &errors.Error{
|
|
|
|
Code: errors.EUnprocessableEntity,
|
2020-06-29 18:16:55 +00:00
|
|
|
Err: err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
actions, err := reqBody.validActions()
|
|
|
|
if err != nil {
|
|
|
|
s.api.Err(w, r, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
applyOpts := []ApplyOptFn{
|
|
|
|
ApplyWithEnvRefs(reqBody.EnvRefs),
|
2020-06-30 21:54:00 +00:00
|
|
|
ApplyWithTemplate(parsedTemplate),
|
2020-06-29 18:16:55 +00:00
|
|
|
ApplyWithStackID(stackID),
|
|
|
|
}
|
|
|
|
for _, a := range actions.SkipResources {
|
|
|
|
applyOpts = append(applyOpts, ApplyWithResourceSkip(a))
|
|
|
|
}
|
|
|
|
for _, a := range actions.SkipKinds {
|
|
|
|
applyOpts = append(applyOpts, ApplyWithKindSkip(a))
|
|
|
|
}
|
|
|
|
|
|
|
|
auth, err := pctx.GetAuthorizer(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
s.api.Err(w, r, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
userID := auth.GetUserID()
|
|
|
|
|
|
|
|
if reqBody.DryRun {
|
|
|
|
impact, err := s.svc.DryRun(r.Context(), *orgID, userID, applyOpts...)
|
|
|
|
if IsParseErr(err) {
|
2022-02-11 22:28:49 +00:00
|
|
|
s.api.Respond(w, r, http.StatusUnprocessableEntity, RespApplyErr{
|
|
|
|
RespApply: impactToRespApply(impact, err),
|
|
|
|
Code: errors.EUnprocessableEntity,
|
|
|
|
Message: "unprocessable entity",
|
|
|
|
})
|
2020-06-29 18:16:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
s.api.Err(w, r, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-06 17:24:53 +00:00
|
|
|
s.api.Respond(w, r, http.StatusOK, impactToRespApply(impact, nil))
|
2020-06-29 18:16:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
applyOpts = append(applyOpts, ApplyWithSecrets(reqBody.Secrets))
|
|
|
|
|
|
|
|
impact, err := s.svc.Apply(r.Context(), *orgID, userID, applyOpts...)
|
|
|
|
if err != nil && !IsParseErr(err) {
|
|
|
|
s.api.Err(w, r, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-06 17:24:53 +00:00
|
|
|
s.api.Respond(w, r, http.StatusCreated, impactToRespApply(impact, err))
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *HTTPServerTemplates) encResp(w http.ResponseWriter, r *http.Request, enc encoder, code int, res interface{}) {
|
|
|
|
w.WriteHeader(code)
|
|
|
|
if err := enc.Encode(res); err != nil {
|
2021-03-30 18:10:02 +00:00
|
|
|
s.api.Err(w, r, &errors.Error{
|
2020-06-29 18:16:55 +00:00
|
|
|
Msg: fmt.Sprintf("unable to marshal; Err: %v", err),
|
2021-03-30 18:10:02 +00:00
|
|
|
Code: errors.EInternal,
|
2020-06-29 18:16:55 +00:00
|
|
|
Err: err,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-06 17:24:53 +00:00
|
|
|
func impactToRespApply(impact ImpactSummary, err error) RespApply {
|
|
|
|
out := RespApply{
|
|
|
|
Sources: append([]string{}, impact.Sources...), // guarantee non nil slice
|
|
|
|
StackID: impact.StackID.String(),
|
|
|
|
Diff: impact.Diff,
|
|
|
|
Summary: impact.Summary,
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
out.Errors = convertParseErr(err)
|
|
|
|
}
|
|
|
|
if out.Diff.Buckets == nil {
|
|
|
|
out.Diff.Buckets = []DiffBucket{}
|
|
|
|
}
|
|
|
|
if out.Diff.Checks == nil {
|
|
|
|
out.Diff.Checks = []DiffCheck{}
|
|
|
|
}
|
|
|
|
if out.Diff.Dashboards == nil {
|
|
|
|
out.Diff.Dashboards = []DiffDashboard{}
|
|
|
|
}
|
|
|
|
if out.Diff.Labels == nil {
|
|
|
|
out.Diff.Labels = []DiffLabel{}
|
|
|
|
}
|
|
|
|
if out.Diff.LabelMappings == nil {
|
|
|
|
out.Diff.LabelMappings = []DiffLabelMapping{}
|
|
|
|
}
|
|
|
|
if out.Diff.NotificationEndpoints == nil {
|
|
|
|
out.Diff.NotificationEndpoints = []DiffNotificationEndpoint{}
|
|
|
|
}
|
|
|
|
if out.Diff.NotificationRules == nil {
|
|
|
|
out.Diff.NotificationRules = []DiffNotificationRule{}
|
|
|
|
}
|
|
|
|
if out.Diff.NotificationRules == nil {
|
|
|
|
out.Diff.NotificationRules = []DiffNotificationRule{}
|
|
|
|
}
|
|
|
|
if out.Diff.Tasks == nil {
|
|
|
|
out.Diff.Tasks = []DiffTask{}
|
|
|
|
}
|
|
|
|
if out.Diff.Telegrafs == nil {
|
|
|
|
out.Diff.Telegrafs = []DiffTelegraf{}
|
|
|
|
}
|
|
|
|
if out.Diff.Variables == nil {
|
|
|
|
out.Diff.Variables = []DiffVariable{}
|
|
|
|
}
|
|
|
|
|
|
|
|
if out.Summary.Buckets == nil {
|
|
|
|
out.Summary.Buckets = []SummaryBucket{}
|
|
|
|
}
|
|
|
|
if out.Summary.Checks == nil {
|
|
|
|
out.Summary.Checks = []SummaryCheck{}
|
|
|
|
}
|
|
|
|
if out.Summary.Dashboards == nil {
|
|
|
|
out.Summary.Dashboards = []SummaryDashboard{}
|
|
|
|
}
|
|
|
|
if out.Summary.Labels == nil {
|
|
|
|
out.Summary.Labels = []SummaryLabel{}
|
|
|
|
}
|
|
|
|
if out.Summary.LabelMappings == nil {
|
|
|
|
out.Summary.LabelMappings = []SummaryLabelMapping{}
|
|
|
|
}
|
|
|
|
if out.Summary.NotificationEndpoints == nil {
|
|
|
|
out.Summary.NotificationEndpoints = []SummaryNotificationEndpoint{}
|
|
|
|
}
|
|
|
|
if out.Summary.NotificationRules == nil {
|
|
|
|
out.Summary.NotificationRules = []SummaryNotificationRule{}
|
|
|
|
}
|
|
|
|
if out.Summary.NotificationRules == nil {
|
|
|
|
out.Summary.NotificationRules = []SummaryNotificationRule{}
|
|
|
|
}
|
|
|
|
if out.Summary.Tasks == nil {
|
|
|
|
out.Summary.Tasks = []SummaryTask{}
|
|
|
|
}
|
|
|
|
if out.Summary.TelegrafConfigs == nil {
|
|
|
|
out.Summary.TelegrafConfigs = []SummaryTelegraf{}
|
|
|
|
}
|
|
|
|
if out.Summary.Variables == nil {
|
|
|
|
out.Summary.Variables = []SummaryVariable{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
2020-06-29 18:16:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func formatSources(sources []string) string {
|
|
|
|
return strings.Join(sources, "; ")
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeWithEncoding(r *http.Request, v interface{}) (Encoding, error) {
|
2020-06-30 21:54:00 +00:00
|
|
|
encoding := templateEncoding(r.Header.Get("Content-Type"))
|
2020-06-29 18:16:55 +00:00
|
|
|
|
|
|
|
var dec interface{ Decode(interface{}) error }
|
|
|
|
switch encoding {
|
|
|
|
case EncodingJsonnet:
|
|
|
|
dec = jsonnet.NewDecoder(r.Body)
|
|
|
|
case EncodingYAML:
|
|
|
|
dec = yaml.NewDecoder(r.Body)
|
|
|
|
default:
|
|
|
|
dec = json.NewDecoder(r.Body)
|
|
|
|
}
|
|
|
|
|
|
|
|
return encoding, dec.Decode(v)
|
|
|
|
}
|
|
|
|
|
2020-06-30 21:54:00 +00:00
|
|
|
func templateEncoding(contentType string) Encoding {
|
2020-06-29 18:16:55 +00:00
|
|
|
switch contentType {
|
|
|
|
case "application/x-jsonnet":
|
|
|
|
return EncodingJsonnet
|
|
|
|
case "text/yml", "application/x-yaml":
|
|
|
|
return EncodingYAML
|
|
|
|
default:
|
|
|
|
return EncodingJSON
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertEncoding(ct, rawURL string) Encoding {
|
|
|
|
ct = strings.ToLower(ct)
|
|
|
|
urlBase := path.Ext(rawURL)
|
|
|
|
switch {
|
|
|
|
case ct == "jsonnet" || urlBase == ".jsonnet":
|
|
|
|
return EncodingJsonnet
|
|
|
|
case ct == "json" || urlBase == ".json":
|
|
|
|
return EncodingJSON
|
|
|
|
case ct == "yml" || ct == "yaml" || urlBase == ".yml" || urlBase == ".yaml":
|
|
|
|
return EncodingYAML
|
|
|
|
default:
|
|
|
|
return EncodingSource
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-06 17:24:53 +00:00
|
|
|
type encoder interface {
|
|
|
|
Encode(interface{}) error
|
|
|
|
}
|
|
|
|
|
2020-06-29 18:16:55 +00:00
|
|
|
func newJSONEnc(w io.Writer) encoder {
|
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
enc.SetIndent("", "\t")
|
|
|
|
return enc
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertParseErr(err error) []ValidationErr {
|
|
|
|
pErr, ok := err.(ParseError)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return pErr.ValidationErrs()
|
|
|
|
}
|
|
|
|
|
2021-03-30 18:10:02 +00:00
|
|
|
func newDecodeErr(encoding string, err error) *errors.Error {
|
|
|
|
return &errors.Error{
|
2020-06-29 18:16:55 +00:00
|
|
|
Msg: fmt.Sprintf("unable to unmarshal %s", encoding),
|
2021-03-30 18:10:02 +00:00
|
|
|
Code: errors.EInvalid,
|
2020-06-29 18:16:55 +00:00
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|