Skip to content

Commit

Permalink
Add configurable service-to-service auth methods (encoredev#750)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DomBlack authored Jun 7, 2023
1 parent e7f6435 commit a3ef00b
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 8 deletions.
20 changes: 17 additions & 3 deletions cli/daemon/run/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}

Expand Down
5 changes: 2 additions & 3 deletions runtime/appruntime/apisdk/api/call_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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)
}
Expand Down
10 changes: 9 additions & 1 deletion runtime/appruntime/apisdk/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -164,6 +171,7 @@ func NewServer(
private: newRouter(),
privateFallback: newRouter(),
encore: newRouter(),
internalAuth: svcAuth,
}

// Configure CORS
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion runtime/appruntime/apisdk/api/svcauth/pkgfn.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"encore.dev/appruntime/apisdk/api/transport"
"encore.dev/appruntime/exported/config"
)

const (
Expand All @@ -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.
Expand All @@ -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
}
16 changes: 16 additions & 0 deletions runtime/appruntime/exported/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down

0 comments on commit a3ef00b

Please sign in to comment.