From a3ef00ba52e8712cc2901ef2d9bfae635f11cbda Mon Sep 17 00:00:00 2001 From: Dominic Black Date: Wed, 7 Jun 2023 09:15:40 +0000 Subject: [PATCH] Add configurable service-to-service auth methods (#750) Currently we default to the `noop` method in service-to-service calls, however this needs to be configurable, so different environments can use different methods. This commit thus adds an initial version of auth config --- cli/daemon/run/manager.go | 20 ++++++++++++++++--- runtime/appruntime/apisdk/api/call_meta.go | 5 ++--- runtime/appruntime/apisdk/api/server.go | 10 +++++++++- .../appruntime/apisdk/api/svcauth/pkgfn.go | 17 +++++++++++++++- runtime/appruntime/exported/config/config.go | 16 +++++++++++++++ 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/cli/daemon/run/manager.go b/cli/daemon/run/manager.go index 96be3b32f7..216f333373 100644 --- a/cli/daemon/run/manager.go +++ b/cli/daemon/run/manager.go @@ -154,14 +154,25 @@ func (mgr *Manager) generateServiceDiscoveryMap(p generateConfigParams) (map[str services[svc.Name] = config.Service{ Name: svc.Name, // For now all services are hosted by the same running instance - URL: p.APIBaseURL, - Protocol: config.Http, + URL: p.APIBaseURL, + Protocol: config.Http, + ServiceAuth: mgr.getInternalServiceToServiceAuthMethod(), } } return services, nil } +// getInternalServiceToServiceAuthMethod returns the auth method to use +// when making service to service calls locally. +// +// This currently just returns the noop auth method, but in the future +// this function will allow us to use environmental variables to configure +// the auth method and test different auth methods locally. +func (mgr *Manager) getInternalServiceToServiceAuthMethod() config.ServiceAuth { + return config.ServiceAuth{Method: "noop"} +} + func (mgr *Manager) generateConfig(p generateConfigParams) (*config.Runtime, error) { envType := encore.EnvDevelopment if p.ForTests { @@ -209,7 +220,10 @@ func (mgr *Manager) generateConfig(p generateConfigParams) (*config.Runtime, err ExtraExposedHeaders: globalCORS.ExposeHeaders, AllowPrivateNetworkAccess: true, }, - ServiceDiscovery: serviceDiscovery, + ServiceDiscovery: serviceDiscovery, + ServiceAuth: []config.ServiceAuth{ + mgr.getInternalServiceToServiceAuthMethod(), + }, ExperimentUseExternalCalls: p.ExternalCalls, } diff --git a/runtime/appruntime/apisdk/api/call_meta.go b/runtime/appruntime/apisdk/api/call_meta.go index d35990a4f6..a91d818657 100644 --- a/runtime/appruntime/apisdk/api/call_meta.go +++ b/runtime/appruntime/apisdk/api/call_meta.go @@ -102,7 +102,7 @@ func (meta CallMeta) AddToRequest(req transport.Transport) error { } // MetaFromRequest reads the metadata from the given request and returns it -func MetaFromRequest(req transport.Transport) (meta CallMeta, err error) { +func (s *Server) MetaFromRequest(req transport.Transport) (meta CallMeta, err error) { // Read the meta version if set and check it's only version 1 // as that's the only version we support if metaVersion, found := req.ReadMeta("Version"); found && metaVersion != "1" { @@ -129,8 +129,7 @@ func MetaFromRequest(req transport.Transport) (meta CallMeta, err error) { // If it was an internal call, read the internal metadata if sendingService, found := req.ReadMeta("Internal-Call"); found { - // TODO(domblack): lookup svc auth method(s) from service name - isInternalCall, err := svcauth.Verify(req, svcauth.Noop) + isInternalCall, err := svcauth.Verify(req, s.internalAuth) if err != nil { return CallMeta{}, fmt.Errorf("failed to verify internal call: %w", err) } diff --git a/runtime/appruntime/apisdk/api/server.go b/runtime/appruntime/apisdk/api/server.go index bdd75879d6..a5f27f7ae9 100644 --- a/runtime/appruntime/apisdk/api/server.go +++ b/runtime/appruntime/apisdk/api/server.go @@ -13,6 +13,7 @@ import ( "github.com/rs/zerolog" encore "encore.dev" + "encore.dev/appruntime/apisdk/api/svcauth" "encore.dev/appruntime/apisdk/api/transport" "encore.dev/appruntime/apisdk/cors" "encore.dev/appruntime/exported/config" @@ -109,6 +110,7 @@ type Server struct { private *httprouter.Router privateFallback *httprouter.Router encore *httprouter.Router + internalAuth []svcauth.ServiceAuth // auth methods for internal service-to-service calls httpsrv *http.Server callCtr uint64 @@ -145,6 +147,11 @@ func NewServer( return router } + svcAuth, err := svcauth.LoadMethods(runtime.ServiceAuth) + if err != nil { + panic(fmt.Errorf("error loading service auth methods: %w", err)) + } + s := &Server{ static: static, runtime: runtime, @@ -164,6 +171,7 @@ func NewServer( private: newRouter(), privateFallback: newRouter(), encore: newRouter(), + internalAuth: svcAuth, } // Configure CORS @@ -317,7 +325,7 @@ func (s *Server) handler(w http.ResponseWriter, req *http.Request) { // Extract the metadata from the request so we can allow access to the private router. // If the metadata is not present, then we assume this is a public request. - callMeta, err := MetaFromRequest(transport.HTTPRequest(req)) + callMeta, err := s.MetaFromRequest(transport.HTTPRequest(req)) if err != nil { s.rootLogger.Error().Err(err).Msg("failed to extract metadata from request") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/runtime/appruntime/apisdk/api/svcauth/pkgfn.go b/runtime/appruntime/apisdk/api/svcauth/pkgfn.go index 36f2b4ea9e..74fefa0ebc 100644 --- a/runtime/appruntime/apisdk/api/svcauth/pkgfn.go +++ b/runtime/appruntime/apisdk/api/svcauth/pkgfn.go @@ -4,6 +4,7 @@ import ( "fmt" "encore.dev/appruntime/apisdk/api/transport" + "encore.dev/appruntime/exported/config" ) const ( @@ -21,7 +22,7 @@ func Sign(method ServiceAuth, req transport.Transport) error { } // Verify verifies the authenticity of the request using the given authentication methods. -func Verify(req transport.Transport, loadedAuthMethods ...ServiceAuth) (internalCall bool, err error) { +func Verify(req transport.Transport, loadedAuthMethods []ServiceAuth) (internalCall bool, err error) { method, found := req.ReadMeta(AuthMethodMetaKey) if !found { // If this is not set, it means that the request is not an internal service to service call. @@ -39,3 +40,17 @@ func Verify(req transport.Transport, loadedAuthMethods ...ServiceAuth) (internal return false, fmt.Errorf("unknown service to service authentication method: %s", method) } + +// LoadMethods loads the service to service authentication methods from the given config. +func LoadMethods(cfg []config.ServiceAuth) (rtn []ServiceAuth, err error) { + for _, authCfg := range cfg { + switch authCfg.Method { + case "noop": + rtn = append(rtn, &noop{}) + default: + return nil, fmt.Errorf("unknown service to service authentication method: %s", authCfg.Method) + } + } + + return rtn, nil +} diff --git a/runtime/appruntime/exported/config/config.go b/runtime/appruntime/exported/config/config.go index a7ea475b2c..5d3d3370b0 100644 --- a/runtime/appruntime/exported/config/config.go +++ b/runtime/appruntime/exported/config/config.go @@ -50,6 +50,13 @@ type Runtime struct { ServiceDiscovery map[string]Service `json:"services,omitempty"` Gateways []Gateway `json:"gateways,omitempty"` // Gateways defines the gateways which should be served by the container + // ServiceAuth defines which authentication method can be used + // when talking to this runtime for internal service-to-service + // calls. + // + // An empty slice means that no service-to-service calls can be made + ServiceAuth []ServiceAuth `json:"service_auth,omitempty"` + // ShutdownTimeout is the duration before non-graceful shutdown is initiated, // meaning connections are closed even if outstanding requests are still in flight. // If zero, it shuts down immediately. @@ -78,6 +85,10 @@ type Service struct { URL string `json:"url"` // Protocol is the protocol that the service talks Protocol SvcProtocol `json:"protocol"` + + // ServiceAuth is the authentication configuration required for + // internal service to service calls being made to this service. + ServiceAuth ServiceAuth `json:"service_auth"` } type SvcProtocol string @@ -86,6 +97,11 @@ const ( Http SvcProtocol = "http" ) +type ServiceAuth struct { + // Method is the name of the authentication method. + Method string `json:"method"` +} + // UnsafeAllOriginWithCredentials can be used to specify that all origins are // allowed to call this API with credentials. It is unsafe and misuse can lead // to security issues. Only use if you know what you're doing.