* feature (auth): required schema changes for auth ldap method (#2669) chore (auth/ldap): move schema changes to next avail migration number * feature (auth/ldap): define AuthMethod and all its value objects (#2703) * feature (auth/ldap): storage protos * feature (auth/ldap): define AuthMethod and all its value objects * feature (auth/ldap): add repo and reading an auth method (#2718) * feature (auth/ldap): add Repository.CreateAuthMethod(...) and Repository.DeleteAuthMethod(...) (#2724) * feature (auth/ldap): add Repository.UpdateAuthMethod(...) (#2739) * feature (auth/ldap): add Account * feature (auth/ldap): add AuthMethod.EnableGroups * fix (auth/ldap): refactor AuthMethod.oplog to enforce proper constraints * feature (auth/ldap): add Account repo functions * refactor (auth/ldap): remove entry attributes from account Realized the entry attributes could have absolutely anything in them (including binary data) and since we absolutely don't have to have them there's just no reason to take on the risk. * feature (auth/ldap): add AuthMethod.UseTokenGroups * feature (auth/ldap): add Authenticate(...) * refactor (auth/ldap): ensure options take a context as the 1st parameter * feature (auth/ldap): add Account attribute maps * chore (auth/ldap): make fmt deltas * feature (auth/ldap): add managed groups (#2760) * tests (auth/ldap): add missing unit test to Repository.DeleteAccount(...) Add bits to test the delete operation when you're not able to generate oplog metadata * feature (auth/ldap): add managed groups fixup! feature (auth/ldap): add managed groups (#2760) * feature (auth/ldap): service proto definition (#2761) * feature (handlers/authmethods): add handlers for ldap auth method operations (#2794) * feature (auth/ldap): add Account attribute maps * chore (auth/ldap): update cap/ldap to latest version * feature (auth/ldap): add ldap api generation definitions * feature (authmethods): add ldap repo NewService(...) * feature (authmethods/ldap): add proper mask_mapping to protobufs * feature (authmethods): add support to get an ldap auth method * refactor (auth/ldap): export TestGenerateCA(...) * feature (authmethods): add support to create an ldap auth method * feature (authmethods): add support to delete an ldap auth method * feature (authmethods): add support to list ldap auth methods * refactor (auth/ldap): make urls optional for NewAuthMethod(...) * refactor (auth/ldap): export ldap.TestInvalidPem * chore: make fmt changes * fix (auth/ldap): properly handle group search config Add constraints and tests to ensure when an ldap AuthMethod.EnableGroups is true, and UseTokenGroups is false; that there's a GroupDn configured for finding a user's associated groups * feature (authmethods): add support to update an ldap auth methods * chore (db/ldap): tmp mv migrations so there's no conflict with ongoing work * feature (verifier): add ldap auth method to verifier bits * fix (controller): prevent panic when controller stops when there's no listener * feature (authmethods): add support to authenticate via ldap auth methods * chore (migrations): fix whitespace in stmt * chore: fmt fixup * tests (auth/ldap): invalid err msg * feature (cli/authmethods): add support for ldap auth-methods CRUD and authenticate (#2810) * feature (authmethods): add CLI support for ldap auth methods CRUD * tests (api/auth): ldap auth method classification tests * feature (authmethods): add CLI support for ldap auth authenticate * feature (auth/ldap): set request timeouts for ldap server connections * feature (handlers/authmethods): handle u_anon listing properly. * feature (account/handers): ldap account and managed group CRUDL APIs (#2852) * feature (auth/ldap) add repository Listing of ManagedGroupMemberAccount * feature (controller/handlers): add ldapRepoFn to accounts service * feature (auth/ldap) register ldap managed group subtype * feature (account/handlers): ldap account CRUDL APIs * feature (controller/handlers): add ldapRepo to managed groups service * feature (account/handlers): ldap managed group CRUDL APIs * docs (domain): add LDAP accounts, auth-methods and managed groups (#2857) * feature (ldap/cli) add ldap accounts and managed groups CRUDL commands (#2856) * fix (handlers/authmethods): fix ldap authorized actions (#2892) * feature (cli/ldap/authenticate): use primary auth method if none is provided (#2890) * fix (auth/ldap): support setting the state attribute * feature (cli/ldap/authenticate): use primary auth method if none is provided * feature (wh/ldap) add tests for new ldap auth method and accounts (#2919) * refactor (migrations/ldap): mv to correct directory * chore: add copyright headers * fix (api/authmethods/ldap): renumber new LdapAuthMethodAttributes field * fix (auth/ldap): allow the auth method state to be updated (#2951) * chore: update sdk and api versions for llb - this is tmp until merging * tests (managed groups): add required errContains for new test
704 lines
27 KiB
Go
704 lines
27 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/textproto"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/auth/oidc"
|
|
"github.com/hashicorp/boundary/internal/daemon/common"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/groups"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/health"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_catalogs"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_sets"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/hosts"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/managed_groups"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/roles"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/scopes"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/sessions"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/users"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers"
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/internal/metric"
|
|
"github.com/hashicorp/boundary/internal/gen/controller/api/services"
|
|
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
|
|
opsservices "github.com/hashicorp/boundary/internal/gen/ops/services"
|
|
"github.com/hashicorp/boundary/internal/observability/event"
|
|
"github.com/hashicorp/go-cleanhttp"
|
|
"github.com/hashicorp/go-secure-stdlib/listenerutil"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/mr-tron/base58"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
|
|
)
|
|
|
|
type HandlerProperties struct {
|
|
ListenerConfig *listenerutil.ListenerConfig
|
|
CancelCtx context.Context
|
|
}
|
|
|
|
const uiPath = "/"
|
|
|
|
// createMuxWithEndpoints performs all response logic for boundary, using isUiRequest
|
|
// for unified logic between responses and headers.
|
|
func createMuxWithEndpoints(c *Controller, props HandlerProperties) (http.Handler, func(req *http.Request) bool, error) {
|
|
grpcGwMux := newGrpcGatewayMux()
|
|
if err := registerGrpcGatewayEndpoints(props.CancelCtx, grpcGwMux, gatewayDialOptions(c.apiGrpcServerListener)...); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/v1/", grpcGwMux)
|
|
mux.Handle(uiPath, handleUi(c))
|
|
|
|
isUiRequest := func(req *http.Request) bool {
|
|
_, p := mux.Handler(req)
|
|
// check to see if the matched pattern is for the ui
|
|
return p == uiPath
|
|
}
|
|
|
|
return mux, isUiRequest, nil
|
|
}
|
|
|
|
// apiHandler returns an http.Handler for the services. This can be used on
|
|
// its own to mount the Controller API within another web server.
|
|
func (c *Controller) apiHandler(props HandlerProperties) (http.Handler, error) {
|
|
mux, isUiRequest, err := createMuxWithEndpoints(c, props)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
corsWrappedHandler := wrapHandlerWithCors(mux, props)
|
|
commonWrappedHandler := wrapHandlerWithCommonFuncs(corsWrappedHandler, c, props)
|
|
callbackInterceptingHandler := wrapHandlerWithCallbackInterceptor(commonWrappedHandler, c)
|
|
printablePathCheckHandler := cleanhttp.PrintablePathCheckHandler(callbackInterceptingHandler, nil)
|
|
eventsHandler, err := common.WrapWithEventsHandler(printablePathCheckHandler, c.conf.Eventer, c.kms, props.ListenerConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metricsHandler := metric.InstrumentApiHandler(eventsHandler)
|
|
|
|
// This wrap MUST be performed last. If you add a new wrapper, do so above.
|
|
return listenerutil.WrapCustomHeadersHandler(metricsHandler, props.ListenerConfig, isUiRequest), nil
|
|
}
|
|
|
|
// GetHealthHandler returns a gRPC Gateway mux that is registered against the
|
|
// controller's gRPC health service to make it accessible from an HTTP API.
|
|
func (c *Controller) GetHealthHandler(lcfg *listenerutil.ListenerConfig) (http.Handler, error) {
|
|
const op = "controller.(Controller).GetHealthHandler"
|
|
if lcfg == nil {
|
|
return nil, fmt.Errorf("%s: received nil listener config", op)
|
|
}
|
|
|
|
healthGrpcGwMux := newGrpcGatewayMux()
|
|
err := registerHealthGrpcGatewayEndpoint(c.baseContext, healthGrpcGwMux, gatewayDialOptions(c.apiGrpcServerListener)...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: failed to register health service handler: %w", op, err)
|
|
}
|
|
|
|
wrapped := wrapHandlerWithCommonFuncs(healthGrpcGwMux, c, HandlerProperties{lcfg, c.baseContext})
|
|
return common.WrapWithEventsHandler(wrapped, c.conf.Eventer, c.kms, lcfg)
|
|
}
|
|
|
|
func registerHealthGrpcGatewayEndpoint(ctx context.Context, gwMux *runtime.ServeMux, dialOptions ...grpc.DialOption) error {
|
|
return opsservices.RegisterHealthServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions)
|
|
}
|
|
|
|
func (c *Controller) registerGrpcServices(s *grpc.Server) error {
|
|
// We have to check against the current services because the gRPC lib treats a duplicate
|
|
// register call as an error and os.Exits.
|
|
currentServices := s.GetServiceInfo()
|
|
|
|
if _, ok := currentServices[services.HostCatalogService_ServiceDesc.ServiceName]; !ok {
|
|
hcs, err := host_catalogs.NewService(c.StaticHostRepoFn, c.PluginHostRepoFn, c.HostPluginRepoFn, c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create host catalog handler service: %w", err)
|
|
}
|
|
services.RegisterHostCatalogServiceServer(s, hcs)
|
|
}
|
|
if _, ok := currentServices[services.HostSetService_ServiceDesc.ServiceName]; !ok {
|
|
hss, err := host_sets.NewService(c.StaticHostRepoFn, c.PluginHostRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create host set handler service: %w", err)
|
|
}
|
|
services.RegisterHostSetServiceServer(s, hss)
|
|
}
|
|
if _, ok := currentServices[services.HostService_ServiceDesc.ServiceName]; !ok {
|
|
hs, err := hosts.NewService(c.StaticHostRepoFn, c.PluginHostRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create host handler service: %w", err)
|
|
}
|
|
services.RegisterHostServiceServer(s, hs)
|
|
}
|
|
if _, ok := currentServices[services.AccountService_ServiceDesc.ServiceName]; !ok {
|
|
accts, err := accounts.NewService(c.baseContext, c.PasswordAuthRepoFn, c.OidcRepoFn, c.LdapRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create account handler service: %w", err)
|
|
}
|
|
services.RegisterAccountServiceServer(s, accts)
|
|
}
|
|
if _, ok := currentServices[services.AuthMethodService_ServiceDesc.ServiceName]; !ok {
|
|
authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn, c.LdapRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth method handler service: %w", err)
|
|
}
|
|
services.RegisterAuthMethodServiceServer(s, authMethods)
|
|
}
|
|
if _, ok := currentServices[services.AuthTokenService_ServiceDesc.ServiceName]; !ok {
|
|
authtoks, err := authtokens.NewService(c.AuthTokenRepoFn, c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth token handler service: %w", err)
|
|
}
|
|
services.RegisterAuthTokenServiceServer(s, authtoks)
|
|
}
|
|
if _, ok := currentServices[services.ScopeService_ServiceDesc.ServiceName]; !ok {
|
|
os, err := scopes.NewService(c.baseContext, c.IamRepoFn, c.kms)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create scope handler service: %w", err)
|
|
}
|
|
services.RegisterScopeServiceServer(s, os)
|
|
}
|
|
if _, ok := currentServices[services.UserService_ServiceDesc.ServiceName]; !ok {
|
|
us, err := users.NewService(c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create user handler service: %w", err)
|
|
}
|
|
services.RegisterUserServiceServer(s, us)
|
|
}
|
|
if _, ok := currentServices[services.TargetService_ServiceDesc.ServiceName]; !ok {
|
|
ts, err := targets.NewService(
|
|
c.baseContext,
|
|
c.kms,
|
|
c.TargetRepoFn,
|
|
c.IamRepoFn,
|
|
c.ServersRepoFn,
|
|
c.SessionRepoFn,
|
|
c.PluginHostRepoFn,
|
|
c.StaticHostRepoFn,
|
|
c.VaultCredentialRepoFn,
|
|
c.StaticCredentialRepoFn,
|
|
c.downstreamWorkers,
|
|
c.workerStatusGracePeriod)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create target handler service: %w", err)
|
|
}
|
|
services.RegisterTargetServiceServer(s, ts)
|
|
}
|
|
if _, ok := currentServices[services.GroupService_ServiceDesc.ServiceName]; !ok {
|
|
gs, err := groups.NewService(c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create group handler service: %w", err)
|
|
}
|
|
services.RegisterGroupServiceServer(s, gs)
|
|
}
|
|
if _, ok := currentServices[services.RoleService_ServiceDesc.ServiceName]; !ok {
|
|
rs, err := roles.NewService(c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create role handler service: %w", err)
|
|
}
|
|
services.RegisterRoleServiceServer(s, rs)
|
|
}
|
|
if _, ok := currentServices[services.SessionService_ServiceDesc.ServiceName]; !ok {
|
|
ss, err := sessions.NewService(c.SessionRepoFn, c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create session handler service: %w", err)
|
|
}
|
|
services.RegisterSessionServiceServer(s, ss)
|
|
}
|
|
if _, ok := currentServices[services.ManagedGroupService_ServiceDesc.ServiceName]; !ok {
|
|
mgs, err := managed_groups.NewService(c.baseContext, c.OidcRepoFn, c.LdapRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create managed groups handler service: %w", err)
|
|
}
|
|
services.RegisterManagedGroupServiceServer(s, mgs)
|
|
}
|
|
if _, ok := currentServices[services.CredentialStoreService_ServiceDesc.ServiceName]; !ok {
|
|
cs, err := credentialstores.NewService(c.baseContext, c.VaultCredentialRepoFn, c.StaticCredentialRepoFn, c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create credential store handler service: %w", err)
|
|
}
|
|
services.RegisterCredentialStoreServiceServer(s, cs)
|
|
}
|
|
if _, ok := currentServices[services.CredentialLibraryService_ServiceDesc.ServiceName]; !ok {
|
|
cl, err := credentiallibraries.NewService(c.VaultCredentialRepoFn, c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create credential library handler service: %w", err)
|
|
}
|
|
services.RegisterCredentialLibraryServiceServer(s, cl)
|
|
}
|
|
if _, ok := currentServices[services.WorkerService_ServiceDesc.ServiceName]; !ok {
|
|
ws, err := workers.NewService(c.baseContext, c.ServersRepoFn, c.IamRepoFn, c.WorkerAuthRepoStorageFn,
|
|
c.downstreamWorkers)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create worker handler service: %w", err)
|
|
}
|
|
services.RegisterWorkerServiceServer(s, ws)
|
|
}
|
|
if _, ok := currentServices[services.CredentialService_ServiceDesc.ServiceName]; !ok {
|
|
c, err := credentials.NewService(c.StaticCredentialRepoFn, c.IamRepoFn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create credential handler service: %w", err)
|
|
}
|
|
services.RegisterCredentialServiceServer(s, c)
|
|
}
|
|
if _, ok := s.GetServiceInfo()[opsservices.HealthService_ServiceDesc.ServiceName]; !ok {
|
|
hs := health.NewService()
|
|
opsservices.RegisterHealthServiceServer(s, hs)
|
|
c.HealthService = hs
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func registerGrpcGatewayEndpoints(ctx context.Context, gwMux *runtime.ServeMux, dialOptions ...grpc.DialOption) error {
|
|
// Register*ServiceHandlerServer methods ignore the passed in context.
|
|
// Passing it in anyways in case this changes in the future.
|
|
if err := services.RegisterHostCatalogServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register host catalog service handler: %w", err)
|
|
}
|
|
if err := services.RegisterHostSetServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register host set service handler: %w", err)
|
|
}
|
|
if err := services.RegisterHostServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register host service handler: %w", err)
|
|
}
|
|
if err := services.RegisterAccountServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register account service handler: %w", err)
|
|
}
|
|
if err := services.RegisterAuthMethodServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register auth method service handler: %w", err)
|
|
}
|
|
if err := services.RegisterAuthTokenServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register auth token service handler: %w", err)
|
|
}
|
|
if err := services.RegisterScopeServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register scope service handler: %w", err)
|
|
}
|
|
if err := services.RegisterUserServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register user service handler: %w", err)
|
|
}
|
|
if err := services.RegisterTargetServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register target service handler: %w", err)
|
|
}
|
|
if err := services.RegisterGroupServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register group service handler: %w", err)
|
|
}
|
|
if err := services.RegisterRoleServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register role service handler: %w", err)
|
|
}
|
|
if err := services.RegisterSessionServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register session service handler: %w", err)
|
|
}
|
|
if err := services.RegisterManagedGroupServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register managed groups service handler: %w", err)
|
|
}
|
|
if err := services.RegisterCredentialStoreServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register credential store service handler: %w", err)
|
|
}
|
|
if err := services.RegisterCredentialLibraryServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register credential library service handler: %w", err)
|
|
}
|
|
if err := services.RegisterWorkerServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register worker service handler: %w", err)
|
|
}
|
|
if err := services.RegisterCredentialServiceHandlerFromEndpoint(ctx, gwMux, gatewayTarget, dialOptions); err != nil {
|
|
return fmt.Errorf("failed to register credential service handler: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProperties) http.Handler {
|
|
const op = "controller.wrapHandlerWithCommonFuncs"
|
|
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
|
|
}
|
|
|
|
disableAuthzFailures := c.conf.DisableAuthorizationFailures ||
|
|
(c.conf.RawConfig.DevController && os.Getenv("BOUNDARY_DEV_SKIP_AUTHZ") != "")
|
|
if disableAuthzFailures {
|
|
event.WriteSysEvent(context.TODO(), op, "AUTHORIZATION CHECKING DISABLED")
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// 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, globals.ContextMaxRequestSizeTypeKey, maxRequestSize)
|
|
}
|
|
|
|
// Add values for authn/authz checking
|
|
requestInfo := authpb.RequestInfo{
|
|
Path: r.URL.Path,
|
|
Method: r.Method,
|
|
DisableAuthzFailures: disableAuthzFailures,
|
|
}
|
|
|
|
requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = auth.GetTokenFromRequest(ctx, c.kms, r)
|
|
|
|
if info, ok := event.RequestInfoFromContext(ctx); ok {
|
|
// piggyback some eventing fields with the auth info proto message
|
|
requestInfo.EventId = info.EventId
|
|
requestInfo.TraceId = info.Id
|
|
requestInfo.ClientIp = info.ClientIp
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
event.WriteError(ctx, op, errors.New("unable to read event request info from context"))
|
|
return
|
|
}
|
|
|
|
// Serialize the request info to send it across the wire to the
|
|
// grpc-gateway via an http header
|
|
requestInfo.Ticket = c.apiGrpcGatewayTicket // allows the grpc-gateway to verify the request info came from it's in-memory companion http proxy
|
|
marshalledRequestInfo, err := proto.Marshal(&requestInfo)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error marshaling request info"))
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Use the default grpc-gateway mapping rule to pass the request info as
|
|
// metadata.
|
|
// See: https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/runtime#DefaultHeaderMatcher
|
|
r.Header.Set("Grpc-Metadata-"+requestInfoMdKey, base58.FastBase58Encoding(marshalledRequestInfo))
|
|
|
|
// Set the context back on the request
|
|
r = r.Clone(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 == nil || !*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
|
|
// When allowed origins is "*" we want to return that rather than
|
|
// round-tripping any user-specified value
|
|
origin = "*"
|
|
|
|
default:
|
|
valid = strutil.StrListContains(allowedOrigins, origin)
|
|
}
|
|
|
|
if !valid {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusForbidden)
|
|
|
|
err := handlers.ApiErrorWithCodeAndMessage(codes.PermissionDenied, "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 cmdAttrs struct {
|
|
Command string `json:"command,omitempty"`
|
|
Attributes any `json:"attributes,omitempty"`
|
|
}
|
|
|
|
func wrapHandlerWithCallbackInterceptor(h http.Handler, c *Controller) http.Handler {
|
|
logCallbackErrors := os.Getenv("BOUNDARY_LOG_CALLBACK_ERRORS") != ""
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
const op = "controller.wrapHandlerWithCallbackInterceptor"
|
|
ctx := req.Context()
|
|
var err error
|
|
id, err := event.NewId(event.IdField)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to create id for event", "method", req.Method, "url", req.URL.RequestURI()))
|
|
return
|
|
}
|
|
info := &event.RequestInfo{
|
|
EventId: id,
|
|
Id: common.GeneratedTraceId(ctx),
|
|
PublicId: "unknown",
|
|
Method: req.Method,
|
|
Path: req.URL.RequestURI(),
|
|
}
|
|
ctx, err = event.NewRequestInfoContext(ctx, info)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
event.WriteError(req.Context(), op, err, event.WithInfoMsg("unable to create context with request info", "method", req.Method, "url", req.URL.RequestURI()))
|
|
return
|
|
}
|
|
// If this doesn't have a callback suffix on a supported action, serve
|
|
// normally
|
|
if !strings.HasSuffix(req.URL.Path, ":authenticate:callback") {
|
|
h.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
req.URL.Path = strings.TrimSuffix(req.URL.Path, ":callback")
|
|
|
|
// How we get the parameters changes based on the method. Right now only
|
|
// GET is supported with query args, but this can support POST with JSON
|
|
// or URL-encoded args. In those cases, the MIME type would have to be
|
|
// checked; for URL-encoded it'd use ParseForm like Get, and for JSON
|
|
// you'd use a json.RawMessage for Attributes consisting of the body. Or
|
|
// something very similar to that.
|
|
var useForm bool
|
|
switch req.Method {
|
|
case http.MethodGet:
|
|
if err := req.ParseForm(); err != nil {
|
|
if logCallbackErrors && c != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("callback error"))
|
|
}
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
useForm = true
|
|
}
|
|
|
|
attrs := &cmdAttrs{
|
|
Command: "callback",
|
|
}
|
|
|
|
switch {
|
|
case useForm:
|
|
if len(req.Form) > 0 {
|
|
values := make(map[string]any, len(req.Form))
|
|
// This won't handle repeated values. That's fine, at least for now.
|
|
// We can address that if needed, which seems unlikely.
|
|
for k := range req.Form {
|
|
values[k] = req.Form.Get(k)
|
|
}
|
|
|
|
if strings.HasSuffix(req.URL.Path, "oidc:authenticate") {
|
|
if s, ok := values["state"].(string); ok {
|
|
stateWrapper, err := oidc.UnwrapMessage(context.Background(), s)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error marshaling state"))
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if stateWrapper.AuthMethodId == "" {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("missing auth method id"))
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
stripped := strings.TrimSuffix(req.URL.Path, "oidc:authenticate")
|
|
req.URL.Path = fmt.Sprintf("%s%s:authenticate", stripped, stateWrapper.AuthMethodId)
|
|
} else {
|
|
event.WriteError(ctx, op, errors.New("missing state parameter"))
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
attrs.Attributes = values
|
|
}
|
|
|
|
attrBytes, err := json.Marshal(attrs)
|
|
if err != nil {
|
|
if logCallbackErrors && c != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error marshaling json"))
|
|
}
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// If there is any existing body, close it as we're going to replace
|
|
// it. It shouldn't be populated in this code path, but you never
|
|
// know.
|
|
if req.Body != nil {
|
|
if err := req.Body.Close(); err != nil {
|
|
if logCallbackErrors && c != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("error closing original request body"))
|
|
}
|
|
}
|
|
}
|
|
bytesReader := bytes.NewReader(attrBytes)
|
|
req.Body = ioutil.NopCloser(bytesReader)
|
|
req.ContentLength = int64(bytesReader.Len())
|
|
req.Header.Set(textproto.CanonicalMIMEHeaderKey("content-type"), "application/json")
|
|
req.Method = http.MethodPost
|
|
}
|
|
|
|
h.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
|
|
/*
|
|
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
|
|
})
|
|
}
|
|
*/
|