Ctrl K
ComunidadGuías…

http/logical.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package http

import (
	"bufio"
	"bytes"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"mime"
	"net"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/errwrap"
	"github.com/hashicorp/go-uuid"
	"github.com/hashicorp/vault/internalshared/namespace"
	"github.com/hashicorp/vault/sdk/helper/consts"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/hashicorp/vault/vault"
	"go.uber.org/atomic"
)

// bufferedReader can be used to replace a request body with a buffered
// version. The Close method invokes the original Closer.
type bufferedReader struct {
	*bufio.Reader
	rOrig io.ReadCloser
}

func newBufferedReader(r io.ReadCloser) *bufferedReader {
	return &bufferedReader{
		Reader: bufio.NewReader(r),
		rOrig:  r,
	}
}

func (b *bufferedReader) Close() error {
	return b.rOrig.Close()
}

const (
	MergePatchContentTypeHeader = "application/merge-patch+json"
	ScimPatchContentTypeHeader  = "application/scim+json"
)

func buildLogicalRequestNoAuth(perfStandby bool, ra *vault.RouterAccess, w http.ResponseWriter, r *http.Request) (*logical.Request, io.ReadCloser, int, error) {
	ns, err := namespace.FromContext(r.Context())
	if err != nil {
		return nil, nil, http.StatusBadRequest, nil
	}
	path := trimPath(ns, r.URL.Path)
	var data map[string]interface{}
	var origBody io.ReadCloser
	var passHTTPReq bool
	var responseWriter http.ResponseWriter

	// Determine the operation
	var op logical.Operation
	switch r.Method {
	case "DELETE":
		op = logical.DeleteOperation
		data = parseQuery(r.URL.Query())
	case "GET":
		op = logical.ReadOperation
		queryVals := r.URL.Query()
		var list bool
		var err error
		listStr := queryVals.Get("list")
		if listStr != "" {
			list, err = strconv.ParseBool(listStr)
			if err != nil {
				return nil, nil, http.StatusBadRequest, nil
			}
			if list {
				queryVals.Del("list")
				op = logical.ListOperation
				if !strings.HasSuffix(path, "/") {
					path += "/"
				}
			}
		}

		data = parseQuery(queryVals)

		switch {
		case strings.HasPrefix(path, "sys/pprof/"):
			passHTTPReq = true
			responseWriter = w
		case path == "sys/storage/raft/snapshot":
			responseWriter = w
		case path == "sys/internal/counters/activity/export":
			responseWriter = w
		case path == "sys/monitor":
			passHTTPReq = true
			responseWriter = w
		}

	case "POST", "PUT", "RECOVER":
		if r.Method == "RECOVER" {
			op = logical.RecoverOperation
		} else {
			op = logical.UpdateOperation
		}

		// Buffer the request body in order to allow us to peek at the beginning
		// without consuming it. This approach involves no copying.
		bufferedBody := newBufferedReader(r.Body)
		r.Body = bufferedBody

		// If we are uploading a snapshot or receiving an ocsp-request (which
		// is der encoded) we don't want to parse it. Instead, we will simply
		// add the HTTP request to the logical request object for later consumption.
		contentType := r.Header.Get("Content-Type")

		if (ra != nil && ra.IsBinaryPath(r.Context(), path)) ||
			path == "sys/storage/raft/snapshot" ||
			path == "sys/storage/raft/snapshot-force" ||
			path == "sys/storage/raft/snapshot-load" {

			passHTTPReq = true
			origBody = r.Body
		} else {
			// Sample the first bytes to determine whether this should be parsed as
			// a form or as JSON. The amount to look ahead (512 bytes) is arbitrary
			// but extremely tolerant (i.e. allowing 511 bytes of leading whitespace
			// and an incorrect content-type).
			head, err := bufferedBody.Peek(512)
			if err != nil && err != bufio.ErrBufferFull && err != io.EOF {
				status := http.StatusBadRequest
				logical.AdjustErrorStatusCode(&status, err)
				return nil, nil, status, fmt.Errorf("error reading data")
			}

			if isForm(head, contentType) {
				formData, err := parseFormRequest(r)
				if err != nil {
					status := http.StatusBadRequest
					logical.AdjustErrorStatusCode(&status, err)
					return nil, nil, status, fmt.Errorf("error parsing form data")
				}

				data = formData
			} else {
				origBody, err = parseJSONRequest(perfStandby, r, w, &data)
				if err == io.EOF {
					data = nil
					err = nil
				}
				if err != nil {
					status := http.StatusBadRequest
					logical.AdjustErrorStatusCode(&status, err)
					return nil, nil, status, fmt.Errorf("error parsing JSON: %w", err)
				}
			}
		}

	case "PATCH":
		op = logical.PatchOperation

		contentTypeHeader := r.Header.Get("Content-Type")
		contentType, _, err := mime.ParseMediaType(contentTypeHeader)
		if err != nil {
			status := http.StatusBadRequest
			logical.AdjustErrorStatusCode(&status, err)
			return nil, nil, status, err
		}

		if contentType != MergePatchContentTypeHeader && contentType != ScimPatchContentTypeHeader {
			return nil, nil, http.StatusUnsupportedMediaType, fmt.Errorf("PATCH requires Content-Type of %s, provided %s", MergePatchContentTypeHeader, contentType)
		}

		origBody, err = parseJSONRequest(perfStandby, r, w, &data)

		if err == io.EOF {
			data = nil
			err = nil
		}

		if err != nil {
			status := http.StatusBadRequest
			logical.AdjustErrorStatusCode(&status, err)
			return nil, nil, status, fmt.Errorf("error parsing JSON")
		}

	case "LIST":
		op = logical.ListOperation
		if !strings.HasSuffix(path, "/") {
			path += "/"
		}

		data = parseQuery(r.URL.Query())
	case "HEAD":
		op = logical.HeaderOperation
		data = parseQuery(r.URL.Query())
	case "OPTIONS":
	default:
		return nil, nil, http.StatusMethodNotAllowed, nil
	}

	// RFC 5785 Redirect, keep the request for auditing purposes
	if r.URL.Path != r.RequestURI {
		passHTTPReq = true
	}

	requestId, err := uuid.GenerateUUID()
	if err != nil {
		return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to generate identifier for the request: %w", err)
	}

	var requiredSnapshotID, recoverSourcePath string

	// check for a read, list, or recover from snapshot request
	switch op {
	case logical.ReadOperation, logical.ListOperation:
		snapshotID, ok := data[VaultSnapshotReadParam]
		if ok && snapshotID != "" {
			requiredSnapshotID = snapshotID.(string)
			delete(data, VaultSnapshotReadParam)
			if len(data) == 0 {
				data = nil
			}
		}
	case logical.UpdateOperation, logical.RecoverOperation:
		snapshotHeaderID := r.Header.Get(VaultSnapshotRecoverHeader)
		if snapshotHeaderID != "" {
			requiredSnapshotID = snapshotHeaderID
		} else {
			queryVals := r.URL.Query()
			if queryVals.Has(VaultSnapshotRecoverParam) {
				snapshotID := queryVals.Get(VaultSnapshotRecoverParam)
				if snapshotID != "" {
					requiredSnapshotID = snapshotID
				}
			}
		}
		if requiredSnapshotID == "" && op == logical.RecoverOperation {
			return nil, nil, http.StatusBadRequest, fmt.Errorf("missing required snapshot ID")
		}

		if requiredSnapshotID != "" {
			op = logical.RecoverOperation
			sourcePath := r.Header.Get(VaultRecoverSourcePathHeader)
			if sourcePath != "" {
				recoverSourcePath = trimPath(ns, sourcePath)
			}
		}
	}

	req := &logical.Request{
		ID:                 requestId,
		Operation:          op,
		Path:               path,
		Data:               data,
		Connection:         getConnection(r),
		Headers:            r.Header,
		RequiresSnapshotID: requiredSnapshotID,
		RecoverSourcePath:  recoverSourcePath,
	}

	if ra != nil && ra.IsLimitedPath(r.Context(), path) {
		req.PathLimited = true
	}

	if passHTTPReq || strings.HasPrefix(path, "identity/scim/") {
		req.HTTPRequest = r
	}
	if responseWriter != nil {
		req.ResponseWriter = logical.NewHTTPResponseWriter(responseWriter)
	}

	return req, origBody, 0, nil
}

func buildLogicalPath(r *http.Request) (string, int, error) {
	ns, err := namespace.FromContext(r.Context())
	if err != nil {
		return "", http.StatusBadRequest, nil
	}

	path := ns.TrimmedPath(strings.TrimPrefix(r.URL.Path, "/v1/"))

	switch r.Method {
	case "GET":
		var (
			list bool
			err  error
		)

		queryVals := r.URL.Query()

		listStr := queryVals.Get("list")
		if listStr != "" {
			list, err = strconv.ParseBool(listStr)
			if err != nil {
				return "", http.StatusBadRequest, nil
			}
			if list {
				if !strings.HasSuffix(path, "/") {
					path += "/"
				}
			}
		}

	case "LIST":
		if !strings.HasSuffix(path, "/") {
			path += "/"
		}
	}

	return path, 0, nil
}

func buildLogicalRequest(core *vault.Core, w http.ResponseWriter, r *http.Request, chrootNamespace string) (*logical.Request, io.ReadCloser, int, error) {
	req, origBody, status, err := buildLogicalRequestNoAuth(core.PerfStandby(), core.RouterAccess(), w, r)
	if err != nil || status != 0 {
		return nil, nil, status, err
	}
	req.ChrootNamespace = chrootNamespace

	req.SetRequiredState(r.Header.Values(VaultIndexHeaderName))
	requestAuth(r, req)

	req, err = requestWrapInfo(r, req)
	if err != nil {
		return nil, nil, http.StatusBadRequest, fmt.Errorf("error parsing X-Vault-Wrap-TTL header: %w", err)
	}

	err = parseMFAHeader(req)
	if err != nil {
		return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse X-Vault-MFA header: %w", err)
	}

	err = requestPolicyOverride(r, req)
	if err != nil {
		return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse %s header: %w", PolicyOverrideHeaderName, err)
	}

	return req, origBody, 0, nil
}

// handleLogical returns a handler for processing logical requests. These requests
// may or may not end up getting forwarded under certain scenarios if the node
// is a performance standby. Some of these cases include:
//   - Perf standby and token with limited use count.
//   - Perf standby and token re-validation needed (e.g. due to invalid token).
//   - Perf standby and control group error.
func handleLogical(core *vault.Core, chrootNamespace string) http.Handler {
	return handleLogicalInternal(core, false, false, chrootNamespace)
}

// handleLogicalWithInjector returns a handler for processing logical requests
// that also have their logical response data injected at the top-level payload.
// All forwarding behavior remains the same as `handleLogical`.
func handleLogicalWithInjector(core *vault.Core, chrootNamespace string) http.Handler {
	return handleLogicalInternal(core, true, false, chrootNamespace)
}

// handleLogicalNoForward returns a handler for processing logical local-only
// requests. These types of requests never forwarded, and return an
// `vault.ErrCannotForwardLocalOnly` error if attempted to do so.
func handleLogicalNoForward(core *vault.Core, chrootNamespace string) http.Handler {
	return handleLogicalInternal(core, false, true, chrootNamespace)
}

func handleLogicalRecovery(raw *vault.RawBackend, token *atomic.String) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		req, _, statusCode, err := buildLogicalRequestNoAuth(false, nil, w, r)
		if err != nil || statusCode != 0 {
			respondError(w, statusCode, err)
			return
		}
		reqToken := r.Header.Get(consts.AuthHeaderName)
		expectedToken := token.Load()

		if reqToken == "" || expectedToken == "" || subtle.ConstantTimeCompare([]byte(reqToken), []byte(expectedToken)) != 1 {
			respondError(w, http.StatusForbidden, nil)
			return
		}

		resp, err := raw.HandleRequest(r.Context(), req)
		if respondErrorCommon(w, req, resp, err) {
			return
		}

		var httpResp *logical.HTTPResponse
		if resp != nil {
			httpResp = logical.LogicalResponseToHTTPResponse(resp)
			httpResp.RequestID = req.ID
		}
		respondOk(w, httpResp)
	})
}

// handleLogicalInternal is a common helper that returns a handler for
// processing logical requests. The behavior depends on the various boolean
// toggles. Refer to usage on functions for possible behaviors.
func handleLogicalInternal(core *vault.Core, injectDataIntoTopLevel bool, noForward bool, chrootNamespace string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		req, origBody, statusCode, err := buildLogicalRequest(core, w, r, chrootNamespace)
		if err != nil || statusCode != 0 {
			respondError(w, statusCode, err)
			return
		}

		// Websockets need to be handled at HTTP layer instead of logical requests.
		ns, err := namespace.FromContext(r.Context())
		if err != nil {
			respondError(w, http.StatusInternalServerError, err)
			return
		}
		trimmedPath := trimPath(ns, r.URL.Path)

		nsPath := ns.Path
		if ns.ID == namespace.RootNamespaceID {
			nsPath = ""
		}
		if websocketPaths.HasPath(trimmedPath) {
			handler := entHandleEventsSubscribe(core, req)
			if handler != nil {
				handler.ServeHTTP(w, r)
				return
			}
		}
		handler := handleEntPaths(nsPath, core, r)
		if handler != nil {
			handler.ServeHTTP(w, r)
			return
		}

		// For binary paths we expect the plugin to read directly from the body so we need to ensure
		// the original body is still available for the forwarding case on entity creation on a perf standby
		var binaryBuf *bytes.Buffer
		ra := core.RouterAccess()
		if ra.IsBinaryPath(r.Context(), trimmedPath) {
			binaryBuf = &bytes.Buffer{}
			binaryTeeReader := io.NopCloser(io.TeeReader(r.Body, binaryBuf))
			r.Body = binaryTeeReader
			r = r.WithContext(logical.CreateContextOriginalBody(r.Context(), binaryTeeReader))
		}

		// Make the internal request. We attach the connection info
		// as well in case this is an authentication request that requires
		// it. Vault core handles stripping this if we need to. This also
		// handles all error cases; if we hit respondLogical, the request is a
		// success.
		resp, ok, needsForward := request(core, w, r, req)
		switch {
		case errwrap.Contains(resp.Error(), consts.ErrOverloaded.Error()):
			respondError(w, http.StatusServiceUnavailable, consts.ErrOverloaded)
			return
		case needsForward && noForward:
			respondError(w, http.StatusBadRequest, vault.ErrCannotForwardLocalOnly)
			return
		case needsForward && !noForward:
			if origBody != nil {
				if binaryBuf != nil && binaryBuf.Len() > 0 {
					// If this is a binary path, we may need to use the buffered body for forwarding
					// since the original body has been consumed by the plugin. We can use the buffered
					// data to create a new reader for the forward request.
					origBody = newMultiReaderCloser(origBody, binaryBuf)
				}
				r.Body = origBody
			}
			forwardRequest(core, w, r)
			return
		case !ok:
			// If not ok, we simply return. The call on request should have
			// taken care of setting the appropriate response code and payload
			// in this case.
			return
		default:
			// For namespaced requests the URL path is rewritten to include the
			// namespace prefix (e.g. /v1/east-1/sys/mounts/sec) before mux
			// dispatch, so those requests never match the /v1/sys/... entries in
			// injectDataIntoTopRoutes and land here with injectDataIntoTopLevel=false.
			// Check the namespace-stripped trimmedPath against the same routes
			// (with /v1/ stripped) to catch that case.
			inject := injectDataIntoTopLevel
			if !inject {
				const v1Prefix = "/v1/"
				for _, r := range injectDataIntoTopRoutes {
					p := strings.TrimPrefix(r, v1Prefix)
					if trimmedPath == p || strings.HasPrefix(trimmedPath, p) {
						inject = true
						break
					}
				}
			}
			// Build and return the proper response if everything is fine.
			respondLogical(core, w, r, req, resp, inject)
			return
		}
	})
}

func respondLogical(core *vault.Core, w http.ResponseWriter, r *http.Request, req *logical.Request, resp *logical.Response, injectDataIntoTopLevel bool) {
	var httpResp *logical.HTTPResponse
	var ret interface{}

	// If vault's core has already written to the response writer do not add any
	// additional output. Headers have already been sent.
	if req != nil && req.ResponseWriter != nil && req.ResponseWriter.Written() {
		return
	}

	if resp != nil {
		if resp.Redirect != "" {
			// If we have a redirect, redirect! We use a 307 code
			// because we don't actually know if its permanent.
			http.Redirect(w, r, resp.Redirect, 307)
			return
		}

		// Check if this is a raw response
		if _, ok := resp.Data[logical.HTTPStatusCode]; ok {
			respondRaw(w, r, resp)
			return
		}

		if resp.WrapInfo != nil && resp.WrapInfo.Token != "" {
			httpResp = &logical.HTTPResponse{
				WrapInfo: &logical.HTTPWrapInfo{
					Token:           resp.WrapInfo.Token,
					Accessor:        resp.WrapInfo.Accessor,
					TTL:             int(resp.WrapInfo.TTL.Seconds()),
					CreationTime:    resp.WrapInfo.CreationTime.Format(time.RFC3339Nano),
					CreationPath:    resp.WrapInfo.CreationPath,
					WrappedAccessor: resp.WrapInfo.WrappedAccessor,
				},
			}
		} else {
			httpResp = logical.LogicalResponseToHTTPResponse(resp)
			httpResp.RequestID = req.ID
		}

		ret = httpResp

		if injectDataIntoTopLevel {
			injector := logical.HTTPSysInjector{
				Response: httpResp,
			}
			ret = injector
		}
	}

	entAdjustResponse(core, w, req)

	// Respond
	respondOk(w, ret)
	return
}

// respondRaw is used when the response is using HTTPContentType and HTTPRawBody
// to change the default response handling. This is only used for specific things like
// returning the CRL information on the PKI backends.
func respondRaw(w http.ResponseWriter, r *http.Request, resp *logical.Response) {
	retErr := func(w http.ResponseWriter, err string) {
		defer logical.IncrementResponseStatusCodeMetric(http.StatusInternalServerError)

		w.Header().Set("X-Vault-Raw-Error", err)
		w.WriteHeader(http.StatusInternalServerError)
		w.Write(nil)
	}

	// Ensure this is never a secret or auth response
	if resp.Secret != nil || resp.Auth != nil {
		retErr(w, "raw responses cannot contain secrets or auth")
		return
	}

	// Get the status code
	statusRaw, ok := resp.Data[logical.HTTPStatusCode]
	if !ok {
		retErr(w, "no status code given")
		return
	}

	var status int
	switch statusRaw.(type) {
	case int:
		status = statusRaw.(int)
	case float64:
		status = int(statusRaw.(float64))
	case json.Number:
		s64, err := statusRaw.(json.Number).Float64()
		if err != nil {
			retErr(w, "cannot decode status code")
			return
		}
		status = int(s64)
	default:
		retErr(w, "cannot decode status code")
		return
	}

	nonEmpty := status != http.StatusNoContent

	var contentType string
	var body []byte

	// Get the content type header; don't require it if the body is empty
	contentTypeRaw, ok := resp.Data[logical.HTTPContentType]
	if !ok && nonEmpty {
		retErr(w, "no content type given")
		return
	}
	if ok {
		contentType, ok = contentTypeRaw.(string)
		if !ok {
			retErr(w, "cannot decode content type")
			return
		}
	}

	if nonEmpty {
		// Get the body
		bodyRaw, ok := resp.Data[logical.HTTPRawBody]
		if !ok {
			goto WRITE_RESPONSE
		}

		switch bodyRaw.(type) {
		case string:
			// This is best effort. The value may already be base64-decoded so
			// if it doesn't work we just use as-is
			bodyDec, err := base64.StdEncoding.DecodeString(bodyRaw.(string))
			if err == nil {
				body = bodyDec
			} else {
				body = []byte(bodyRaw.(string))
			}
		case []byte:
			body = bodyRaw.([]byte)
		default:
			retErr(w, "cannot decode body")
			return
		}
	}

WRITE_RESPONSE:
	// Write the response
	if contentType != "" {
		w.Header().Set("Content-Type", contentType)
	}

	if cacheControl, ok := resp.Data[logical.HTTPCacheControlHeader].(string); ok {
		w.Header().Set("Cache-Control", cacheControl)
	}

	if pragma, ok := resp.Data[logical.HTTPPragmaHeader].(string); ok {
		w.Header().Set("Pragma", pragma)
	}

	if wwwAuthn, ok := resp.Data[logical.HTTPWWWAuthenticateHeader].(string); ok {
		w.Header().Set("WWW-Authenticate", wwwAuthn)
	}

	w.WriteHeader(status)
	w.Write(body)

	logical.IncrementResponseStatusCodeMetric(status)
}

// getConnection is used to format the connection information for
// attaching to a logical request
func getConnection(r *http.Request) (connection *logical.Connection) {
	var remoteAddr string
	var remotePort int

	remoteAddr, port, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		remoteAddr = ""
	} else {
		remotePort, err = strconv.Atoi(port)
		if err != nil {
			remotePort = 0
		}
	}

	connection = &logical.Connection{
		RemoteAddr: remoteAddr,
		RemotePort: remotePort,
		ConnState:  r.TLS,
	}
	return
}