Files
threshold/internal/servers/controller/handler.go
T
Jeff Mitchell 1a1d3058c9 Move verification to a shared package (#197)
* Move verification to a shared package

The verifier is constructed at HTTP request time and added to the
context. It can then be called from appropriate handlers with
standardized error handling after all the routing.

I intentionally did not add to the handlers yet as I am a good distance
into the scope updates and didn't want to cause even more merge
conflicts. So at the moment authz is again disabled, but once the scope
updates are done, this will be integrated back in.
2020-07-16 22:12:26 -04:00

413 lines
14 KiB
Go

package controller
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/watchtower/api"
"github.com/hashicorp/watchtower/globals"
"github.com/hashicorp/watchtower/internal/auth"
"github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/authenticate"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/groups"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/host_catalogs"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/host_sets"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/hosts"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/orgs"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/projects"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/roles"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/users"
)
type HandlerProperties struct {
ListenerConfig *configutil.Listener
}
// Handler returns an http.Handler for the services. This can be used on
// its own to mount the Vault API within another web server.
func (c *Controller) handler(props HandlerProperties) (http.Handler, error) {
// Create the muxer to handle the actual endpoints
mux := http.NewServeMux()
h, err := handleGrpcGateway(c)
if err != nil {
return nil, err
}
mux.Handle("/v1/", h)
mux.Handle("/", handleUi(c))
corsWrappedHandler := wrapHandlerWithCors(mux, props)
commonWrappedHandler := wrapHandlerWithCommonFuncs(corsWrappedHandler, c, props)
return commonWrappedHandler, nil
}
func handleGrpcGateway(c *Controller) (http.Handler, error) {
// Register*ServiceHandlerServer methods ignore the passed in ctx. Using the baseContext now just in case this changes
// in the future, at which point we'll want to be using the baseContext.
ctx := c.baseContext
mux := runtime.NewServeMux(runtime.WithProtoErrorHandler(handlers.ErrorHandler(c.logger)))
hcs, err := host_catalogs.NewService(c.StaticHostRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create host catalog handler service: %w", err)
}
if err := services.RegisterHostCatalogServiceHandlerServer(ctx, mux, hcs); err != nil {
return nil, fmt.Errorf("failed to register host catalog service handler: %w", err)
}
if err := services.RegisterHostSetServiceHandlerServer(ctx, mux, &host_sets.Service{}); err != nil {
return nil, fmt.Errorf("failed to register host set service handler: %w", err)
}
if err := services.RegisterHostServiceHandlerServer(ctx, mux, &hosts.Service{}); err != nil {
return nil, fmt.Errorf("failed to register host service handler: %w", err)
}
auths, err := authenticate.NewService(c.IamRepoFn, c.AuthTokenRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create authentication handler service: %w", err)
}
if err := services.RegisterAuthenticationServiceHandlerServer(ctx, mux, auths); err != nil {
return nil, fmt.Errorf("failed to register authenticate service handler: %w", err)
}
os, err := orgs.NewService(c.IamRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create org handler service: %w", err)
}
if err := services.RegisterOrgServiceHandlerServer(ctx, mux, os); err != nil {
return nil, fmt.Errorf("failed to register org service handler: %w", err)
}
ps, err := projects.NewService(c.IamRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create project handler service: %w", err)
}
if err := services.RegisterProjectServiceHandlerServer(ctx, mux, ps); err != nil {
return nil, fmt.Errorf("failed to register project service handler: %w", err)
}
us, err := users.NewService(c.IamRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create user handler service: %w", err)
}
if err := services.RegisterUserServiceHandlerServer(ctx, mux, us); err != nil {
return nil, fmt.Errorf("failed to register user service handler: %w", err)
}
gs, err := groups.NewService(c.IamRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create group handler service: %w", err)
}
if err := services.RegisterGroupServiceHandlerServer(ctx, mux, gs); err != nil {
return nil, fmt.Errorf("failed to register group service handler: %w", err)
}
rs, err := roles.NewService(c.IamRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create role handler service: %w", err)
}
if err := services.RegisterRoleServiceHandlerServer(ctx, mux, rs); err != nil {
return nil, fmt.Errorf("failed to register role service handler: %w", err)
}
return mux, nil
}
func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProperties) http.Handler {
var maxRequestDuration time.Duration
var maxRequestSize int64
if props.ListenerConfig != nil {
maxRequestDuration = props.ListenerConfig.MaxRequestDuration
maxRequestSize = props.ListenerConfig.MaxRequestSize
}
if maxRequestDuration == 0 {
maxRequestDuration = globals.DefaultMaxRequestDuration
}
if maxRequestSize == 0 {
maxRequestSize = globals.DefaultMaxRequestSize
}
logUrls := os.Getenv("WATCHTOWER_LOG_URLS") != ""
disableAuthzFailures := c.conf.DisableAuthorizationFailures ||
(c.conf.RawConfig.DevController && os.Getenv("WATCHTOWER_DEV_SKIP_AUTHZ") != "")
if disableAuthzFailures {
c.logger.Warn("AUTHORIZATION CHECKING DISABLED")
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if logUrls {
c.logger.Trace("request received", "url", r.URL.String())
}
// Set the Cache-Control header for all responses returned
w.Header().Set("Cache-Control", "no-store")
// Start with the request context and our timeout
ctx, cancelFunc := context.WithTimeout(r.Context(), maxRequestDuration)
defer cancelFunc()
// Add a size limiter if desired
if maxRequestSize > 0 {
ctx = context.WithValue(ctx, "max_request_size", maxRequestSize)
}
// Add values for authn/authz checking
requestInfo := auth.RequestInfo{
Path: r.URL.Path,
Method: r.Method,
DisableAuthzFailures: disableAuthzFailures,
}
requestInfo.PublicId, requestInfo.Token = getTokenFromRequest(c.logger, r)
ctx = auth.NewVerifierContext(ctx, c.logger, c.IamRepoFn, c.AuthTokenRepoFn, requestInfo)
// Set the context back on the request
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
})
}
func wrapHandlerWithCors(h http.Handler, props HandlerProperties) http.Handler {
allowedMethods := []string{
http.MethodDelete,
http.MethodGet,
http.MethodOptions,
http.MethodPost,
http.MethodPatch,
}
allowedOrigins := props.ListenerConfig.CorsAllowedOrigins
allowedHeaders := append([]string{
"Content-Type",
"X-Requested-With",
"Authorization",
}, props.ListenerConfig.CorsAllowedHeaders...)
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !props.ListenerConfig.CorsEnabled {
h.ServeHTTP(w, req)
return
}
origin := req.Header.Get("Origin")
if origin == "" {
// Serve directly
h.ServeHTTP(w, req)
return
}
// Check origin
var valid bool
switch {
case len(allowedOrigins) == 0:
// not valid
case len(allowedOrigins) == 1 && allowedOrigins[0] == "*":
valid = true
default:
valid = strutil.StrListContains(allowedOrigins, origin)
}
if !valid {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
err := &api.Error{
Status: http.StatusForbidden,
Code: "origin forbidden",
}
enc := json.NewEncoder(w)
enc.Encode(err)
return
}
if req.Method == http.MethodOptions &&
!strutil.StrListContains(allowedMethods, req.Header.Get("Access-Control-Request-Method")) {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
// Apply headers for preflight requests
if req.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(allowedHeaders, ", "))
w.Header().Set("Access-Control-Max-Age", "300")
w.WriteHeader(http.StatusNoContent)
return
}
h.ServeHTTP(w, req)
})
}
type tokenFormat int
const (
authTokenTypeUnknown tokenFormat = iota
authTokenTypeBearer
authTokenTypeSplitCookie
)
const (
headerAuthMethod = "Authorization"
httpOnlyCookieName = "wt-http-token-cookie"
jsVisibleCookieName = "wt-js-token-cookie"
)
// getTokenFromRequest pulls the token from either the Authorization header or
// split cookies and parses it. If it cannot be parsed successfully, the issue
// is logged and we return blank, so logic will continue as the anonymous user.
// The public ID and token are returned
func getTokenFromRequest(logger hclog.Logger, req *http.Request) (string, string) {
// First, get the token, either from the authorization header or from split
// cookies
var receivedTokenType tokenFormat
var fullToken string
if authHeader := req.Header.Get("Authorization"); authHeader != "" {
headerSplit := strings.SplitN(strings.TrimSpace(authHeader), " ", 2)
if len(headerSplit) == 2 && strings.EqualFold(strings.TrimSpace(headerSplit[0]), "bearer") {
receivedTokenType = authTokenTypeBearer
fullToken = strings.TrimSpace(headerSplit[1])
}
}
if receivedTokenType != authTokenTypeBearer {
var httpCookiePayload string
var jsCookiePayload string
if hc, err := req.Cookie(httpOnlyCookieName); err == nil {
httpCookiePayload = hc.Value
}
if jc, err := req.Cookie(jsVisibleCookieName); err == nil {
jsCookiePayload = jc.Value
}
if httpCookiePayload != "" && jsCookiePayload != "" {
receivedTokenType = authTokenTypeSplitCookie
fullToken = jsCookiePayload + httpCookiePayload
}
}
if receivedTokenType == authTokenTypeUnknown || fullToken == "" {
// We didn't find auth info or a client screwed up and put in a blank
// header instead of nothing at all, so return blank which will indicate
// the anonymouse user
return "", ""
}
splitFullToken := strings.Split(fullToken, "_")
if len(splitFullToken) != 3 {
logger.Trace("get token from request: unexpected number of segments in token", "expected", 3, "found", len(splitFullToken))
return "", ""
}
token := splitFullToken[2]
publicId := strings.Join(splitFullToken[0:2], "_")
if receivedTokenType == authTokenTypeUnknown || token == "" || publicId == "" {
logger.Trace("get token from request: after parsing, could not find valid token")
return "", ""
}
return publicId, token
}
/*
func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers, headersOK := r.Header[textproto.CanonicalMIMEHeaderKey("X-Forwarded-For")]
if !headersOK || len(headers) == 0 {
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, fmt.Errorf("missing x-forwarded-for header and configured to reject when not present"))
return
}
host, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
// If not rejecting treat it like we just don't have a valid
// header because we can't do a comparison against an address we
// can't understand
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client hostport: {{err}}", err))
return
}
addr, err := sockaddr.NewIPAddr(host)
if err != nil {
// We treat this the same as the case above
if !rejectNotPresent {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client address: {{err}}", err))
return
}
var found bool
for _, authz := range authorizedAddrs {
if authz.Contains(addr) {
found = true
break
}
}
if !found {
// If we didn't find it and aren't configured to reject, simply
// don't trust it
if !rejectNonAuthz {
h.ServeHTTP(w, r)
return
}
respondError(w, http.StatusBadRequest, fmt.Errorf("client address not authorized for x-forwarded-for and configured to reject connection"))
return
}
// At this point we have at least one value and it's authorized
// Split comma separated ones, which are common. This brings it in line
// to the multiple-header case.
var acc []string
for _, header := range headers {
vals := strings.Split(header, ",")
for _, v := range vals {
acc = append(acc, strings.TrimSpace(v))
}
}
indexToUse := len(acc) - 1 - hopSkips
if indexToUse < 0 {
// This is likely an error in either configuration or other
// infrastructure. We could either deny the request, or we
// could simply not trust the value. Denying the request is
// "safer" since if this logic is configured at all there may
// be an assumption it can always be trusted. Given that we can
// deny accepting the request at all if it's not from an
// authorized address, if we're at this point the address is
// authorized (or we've turned off explicit rejection) and we
// should assume that what comes in should be properly
// formatted.
respondError(w, http.StatusBadRequest, fmt.Errorf("malformed x-forwarded-for configuration or request, hops to skip (%d) would skip before earliest chain link (chain length %d)", hopSkips, len(headers)))
return
}
r.RemoteAddr = net.JoinHostPort(acc[indexToUse], port)
h.ServeHTTP(w, r)
return
})
}
*/