github.com/avenga/couper@v1.12.2/config/configload/load.go (about)

     1  package configload
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/hcl/v2"
    11  	"github.com/hashicorp/hcl/v2/gohcl"
    12  	"github.com/hashicorp/hcl/v2/hclparse"
    13  	"github.com/hashicorp/hcl/v2/hclsyntax"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/zclconf/go-cty/cty"
    16  
    17  	"github.com/avenga/couper/config"
    18  	hclbody "github.com/avenga/couper/config/body"
    19  	"github.com/avenga/couper/config/configload/collect"
    20  	configfile "github.com/avenga/couper/config/configload/file"
    21  	"github.com/avenga/couper/config/parser"
    22  	"github.com/avenga/couper/config/reader"
    23  	"github.com/avenga/couper/errors"
    24  	"github.com/avenga/couper/eval"
    25  	"github.com/avenga/couper/eval/lib"
    26  	"github.com/avenga/couper/internal/seetie"
    27  )
    28  
    29  const (
    30  	api             = "api"
    31  	backend         = "backend"
    32  	defaults        = "defaults"
    33  	definitions     = "definitions"
    34  	endpoint        = "endpoint"
    35  	environment     = "environment"
    36  	environmentVars = "environment_variables"
    37  	errorHandler    = "error_handler"
    38  	files           = "files"
    39  	nameLabel       = "name"
    40  	oauth2          = "oauth2"
    41  	proxy           = "proxy"
    42  	request         = "request"
    43  	server          = "server"
    44  	settings        = "settings"
    45  	spa             = "spa"
    46  	tls             = "tls"
    47  	tokenRequest    = "beta_token_request"
    48  )
    49  
    50  var defaultsConfig *config.Defaults
    51  var evalContext *eval.Context
    52  var envContext *hcl.EvalContext
    53  var pathBearingAttributesMap map[string]struct{}
    54  
    55  func init() {
    56  	pathBearingAttributes := []string{
    57  		"bootstrap_file",
    58  		"ca_certificate_file",
    59  		"ca_file",
    60  		"client_certificate_file",
    61  		"client_private_key_file",
    62  		"document_root",
    63  		"error_file",
    64  		"file",
    65  		"htpasswd_file",
    66  		"idp_metadata_file",
    67  		"jwks_url",
    68  		"key_file",
    69  		"leaf_certificate_file",
    70  		"permissions_map_file",
    71  		"private_key_file",
    72  		"public_key_file",
    73  		"roles_map_file",
    74  		"server_ca_certificate_file",
    75  		"signing_key_file",
    76  	}
    77  
    78  	pathBearingAttributesMap = make(map[string]struct{})
    79  	for _, attributeName := range pathBearingAttributes {
    80  		pathBearingAttributesMap[attributeName] = struct{}{}
    81  	}
    82  }
    83  
    84  func updateContext(body hcl.Body, srcBytes [][]byte, environment string) hcl.Diagnostics {
    85  	defaultsBlock := &config.DefaultsBlock{}
    86  	// defaultsCtx is a temporary one to allow env variables and functions for defaults {}
    87  	defaultsCtx := eval.NewContext(srcBytes, nil, environment).HCLContext()
    88  	if diags := gohcl.DecodeBody(body, defaultsCtx, defaultsBlock); diags.HasErrors() {
    89  		return diags
    90  	}
    91  	defaultsConfig = defaultsBlock.Defaults // global assign
    92  
    93  	// We need the "envContext" to be able to resolve absolute paths in the config.
    94  	evalContext = eval.NewContext(srcBytes, defaultsConfig, environment)
    95  	envContext = evalContext.HCLContext() // global assign
    96  
    97  	return nil
    98  }
    99  
   100  func parseFile(filePath string, srcBytes *[][]byte) (*hclsyntax.Body, error) {
   101  	src, err := os.ReadFile(filePath)
   102  	if err != nil {
   103  		return nil, fmt.Errorf("failed to load configuration: %w", err)
   104  	}
   105  
   106  	*srcBytes = append(*srcBytes, src)
   107  
   108  	parsed, diags := hclparse.NewParser().ParseHCLFile(filePath)
   109  	if diags.HasErrors() {
   110  		return nil, diags
   111  	}
   112  
   113  	return parsed.Body.(*hclsyntax.Body), nil
   114  }
   115  
   116  func parseFiles(files configfile.Files) ([]*hclsyntax.Body, [][]byte, error) {
   117  	var (
   118  		srcBytes     [][]byte
   119  		parsedBodies []*hclsyntax.Body
   120  	)
   121  
   122  	for _, file := range files {
   123  		if file.IsDir {
   124  			childBodies, bytes, err := parseFiles(file.Children)
   125  			if err != nil {
   126  				return nil, bytes, err
   127  			}
   128  
   129  			parsedBodies = append(parsedBodies, childBodies...)
   130  			srcBytes = append(srcBytes, bytes...)
   131  		} else {
   132  			body, err := parseFile(file.Path, &srcBytes)
   133  			if err != nil {
   134  				return nil, srcBytes, err
   135  			}
   136  			parsedBodies = append(parsedBodies, body)
   137  		}
   138  	}
   139  
   140  	return parsedBodies, srcBytes, nil
   141  }
   142  
   143  func bodiesToConfig(parsedBodies []*hclsyntax.Body, srcBytes [][]byte, env string, logger *logrus.Entry) (*config.Couper, error) {
   144  	deprecate(parsedBodies, logger)
   145  
   146  	defaultsBlock, err := mergeDefaults(parsedBodies)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	defs := &hclsyntax.Body{
   152  		Blocks: hclsyntax.Blocks{defaultsBlock},
   153  	}
   154  
   155  	if diags := updateContext(defs, srcBytes, env); diags.HasErrors() {
   156  		return nil, diags
   157  	}
   158  
   159  	for _, body := range parsedBodies {
   160  		if err = absolutizePaths(body); err != nil {
   161  			return nil, err
   162  		}
   163  
   164  		if err = validateBody(body, false); err != nil {
   165  			return nil, err
   166  		}
   167  	}
   168  
   169  	settingsBlock := mergeSettings(parsedBodies)
   170  
   171  	definitionsBlock, proxies, err := mergeDefinitions(parsedBodies)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	serverBlocks, err := mergeServers(parsedBodies, proxies)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	configBlocks := serverBlocks
   182  	configBlocks = append(configBlocks, definitionsBlock)
   183  	configBlocks = append(configBlocks, defaultsBlock)
   184  	configBlocks = append(configBlocks, settingsBlock)
   185  
   186  	configBody := &hclsyntax.Body{
   187  		Blocks: configBlocks,
   188  	}
   189  
   190  	if err = validateBody(configBody, len(parsedBodies) > 1); err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	conf, err := LoadConfig(configBody)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  
   199  	return conf, nil
   200  }
   201  
   202  func LoadFiles(filesList []string, env string, logger *logrus.Entry) (*config.Couper, error) {
   203  	configFiles, err := configfile.NewFiles(filesList)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	parsedBodies, srcBytes, err := parseFiles(configFiles)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	if len(srcBytes) == 0 {
   214  		return nil, fmt.Errorf("missing configuration files")
   215  	}
   216  
   217  	errorBeforeRetry := preprocessEnvironmentBlocks(parsedBodies, env)
   218  
   219  	if env == "" {
   220  		settingsBlock := mergeSettings(parsedBodies)
   221  		confSettings := &config.Settings{}
   222  		if diags := gohcl.DecodeBody(settingsBlock.Body, nil, confSettings); diags.HasErrors() {
   223  			return nil, diags
   224  		}
   225  		if confSettings.Environment != "" {
   226  			return LoadFiles(filesList, confSettings.Environment, logger)
   227  		}
   228  	}
   229  
   230  	if errorBeforeRetry != nil {
   231  		return nil, errorBeforeRetry
   232  	}
   233  
   234  	conf, err := bodiesToConfig(parsedBodies, srcBytes, env, logger)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	conf.Files = configFiles
   239  
   240  	return conf, nil
   241  }
   242  
   243  func LoadFile(file, env string) (*config.Couper, error) {
   244  	return LoadFiles([]string{file}, env, nil)
   245  }
   246  
   247  type testContent struct {
   248  	filename string
   249  	src      []byte
   250  }
   251  
   252  func loadTestContents(tcs []testContent) (*config.Couper, error) {
   253  	var (
   254  		parsedBodies []*hclsyntax.Body
   255  		srcs         [][]byte
   256  	)
   257  
   258  	for _, tc := range tcs {
   259  		hclBody, err := parser.Load(tc.src, tc.filename)
   260  		if err != nil {
   261  			return nil, err
   262  		}
   263  
   264  		parsedBodies = append(parsedBodies, hclBody)
   265  		srcs = append(srcs, tc.src)
   266  	}
   267  
   268  	return bodiesToConfig(parsedBodies, srcs, "", nil)
   269  }
   270  
   271  func LoadBytes(src []byte, filename string) (*config.Couper, error) {
   272  	return LoadBytesEnv(src, filename, "")
   273  }
   274  
   275  func LoadBytesEnv(src []byte, filename, env string) (*config.Couper, error) {
   276  	hclBody, err := parser.Load(src, filename)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	if err = validateBody(hclBody, false); err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	return bodiesToConfig([]*hclsyntax.Body{hclBody}, [][]byte{src}, env, nil)
   286  }
   287  
   288  func LoadConfig(body *hclsyntax.Body) (*config.Couper, error) {
   289  	var err error
   290  
   291  	if diags := ValidateConfigSchema(body, &config.Couper{}); diags.HasErrors() {
   292  		return nil, diags
   293  	}
   294  
   295  	helper, err := newHelper(body)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	for _, outerBlock := range helper.content.Blocks {
   301  		switch outerBlock.Type {
   302  		case definitions:
   303  			backendContent, leftOver, diags := outerBlock.Body.PartialContent(backendBlockSchema)
   304  			if diags.HasErrors() {
   305  				return nil, diags
   306  			}
   307  
   308  			// backends first
   309  			if backendContent != nil {
   310  				for _, be := range backendContent.Blocks {
   311  					helper.addBackend(be)
   312  				}
   313  
   314  				if err = helper.configureDefinedBackends(); err != nil {
   315  					return nil, err
   316  				}
   317  			}
   318  
   319  			// decode all other blocks into definition struct
   320  			if diags = gohcl.DecodeBody(leftOver, helper.context, helper.config.Definitions); diags.HasErrors() {
   321  				return nil, diags
   322  			}
   323  
   324  			if err = helper.configureACBackends(); err != nil {
   325  				return nil, err
   326  			}
   327  
   328  			acErrorHandler := collect.ErrorHandlerSetters(helper.config.Definitions)
   329  			if err = configureErrorHandler(acErrorHandler, helper); err != nil {
   330  				return nil, err
   331  			}
   332  
   333  		case settings:
   334  			if diags := gohcl.DecodeBody(outerBlock.Body, helper.context, helper.config.Settings); diags.HasErrors() {
   335  				return nil, diags
   336  			}
   337  		}
   338  	}
   339  
   340  	// Prepare dynamic functions
   341  	for _, profile := range helper.config.Definitions.JWTSigningProfile {
   342  		if profile.Headers != nil {
   343  			expression, _ := profile.Headers.Value(nil)
   344  			headers := seetie.ValueToMap(expression)
   345  
   346  			if _, exists := headers["alg"]; exists {
   347  				return nil, errors.Configuration.Label(profile.Name).With(fmt.Errorf(`"alg" cannot be set via "headers"`))
   348  			}
   349  		}
   350  	}
   351  
   352  	for _, saml := range helper.config.Definitions.SAML {
   353  		metadata, err := reader.ReadFromFile("saml2 idp_metadata_file", saml.IdpMetadataFile)
   354  		if err != nil {
   355  			return nil, errors.Configuration.Label(saml.Name).With(err)
   356  		}
   357  		saml.MetadataBytes = metadata
   358  	}
   359  
   360  	jwtSigningConfigs := make(map[string]*lib.JWTSigningConfig)
   361  	for _, profile := range helper.config.Definitions.JWTSigningProfile {
   362  		signConf, err := lib.NewJWTSigningConfigFromJWTSigningProfile(profile, nil)
   363  		if err != nil {
   364  			return nil, errors.Configuration.Label(profile.Name).With(err)
   365  		}
   366  		jwtSigningConfigs[profile.Name] = signConf
   367  	}
   368  	for _, jwt := range helper.config.Definitions.JWT {
   369  		signConf, err := lib.NewJWTSigningConfigFromJWT(jwt)
   370  		if err != nil {
   371  			return nil, errors.Configuration.Label(jwt.Name).With(err)
   372  		}
   373  		if signConf != nil {
   374  			jwtSigningConfigs[jwt.Name] = signConf
   375  		}
   376  	}
   377  
   378  	helper.config.Context = helper.config.Context.(*eval.Context).
   379  		WithJWTSigningConfigs(jwtSigningConfigs).
   380  		WithOAuth2AC(helper.config.Definitions.OAuth2AC).
   381  		WithSAML(helper.config.Definitions.SAML)
   382  
   383  	definedACs := make(map[string]struct{})
   384  	for _, ac := range helper.config.Definitions.BasicAuth {
   385  		definedACs[ac.Name] = struct{}{}
   386  	}
   387  	for _, ac := range helper.config.Definitions.JWT {
   388  		definedACs[ac.Name] = struct{}{}
   389  	}
   390  	for _, ac := range helper.config.Definitions.OAuth2AC {
   391  		definedACs[ac.Name] = struct{}{}
   392  	}
   393  	for _, ac := range helper.config.Definitions.OIDC {
   394  		definedACs[ac.Name] = struct{}{}
   395  	}
   396  	for _, ac := range helper.config.Definitions.SAML {
   397  		definedACs[ac.Name] = struct{}{}
   398  	}
   399  
   400  	// Read per server block and merge backend settings which results in a final server configuration.
   401  	for _, serverBlock := range hclbody.BlocksOfType(body, server) {
   402  		serverConfig := &config.Server{}
   403  		if diags := gohcl.DecodeBody(serverBlock.Body, helper.context, serverConfig); diags.HasErrors() {
   404  			return nil, diags
   405  		}
   406  
   407  		// Set the server name since gohcl.DecodeBody decoded the body and not the block.
   408  		if len(serverBlock.Labels) > 0 {
   409  			serverConfig.Name = serverBlock.Labels[0]
   410  		}
   411  
   412  		if err := checkReferencedAccessControls(serverBlock.Body, serverConfig.AccessControl, serverConfig.DisableAccessControl, definedACs); err != nil {
   413  			return nil, err
   414  		}
   415  
   416  		for _, fileConfig := range serverConfig.Files {
   417  			if err := checkReferencedAccessControls(fileConfig.HCLBody(), fileConfig.AccessControl, fileConfig.DisableAccessControl, definedACs); err != nil {
   418  				return nil, err
   419  			}
   420  		}
   421  
   422  		for _, spaConfig := range serverConfig.SPAs {
   423  			if err := checkReferencedAccessControls(spaConfig.HCLBody(), spaConfig.AccessControl, spaConfig.DisableAccessControl, definedACs); err != nil {
   424  				return nil, err
   425  			}
   426  		}
   427  
   428  		// Read api blocks and merge backends with server and definitions backends.
   429  		for _, apiConfig := range serverConfig.APIs {
   430  			apiBody := apiConfig.HCLBody()
   431  
   432  			if apiConfig.AllowedMethods != nil && len(apiConfig.AllowedMethods) > 0 {
   433  				if err = validMethods(apiConfig.AllowedMethods, apiBody.Attributes["allowed_methods"]); err != nil {
   434  					return nil, err
   435  				}
   436  			}
   437  
   438  			if err := checkReferencedAccessControls(apiBody, apiConfig.AccessControl, apiConfig.DisableAccessControl, definedACs); err != nil {
   439  				return nil, err
   440  			}
   441  
   442  			rp := apiBody.Attributes["required_permission"]
   443  			if rp != nil {
   444  				apiConfig.RequiredPermission = rp.Expr
   445  			}
   446  
   447  			err = refineEndpoints(helper, apiConfig.Endpoints, true, definedACs)
   448  			if err != nil {
   449  				return nil, err
   450  			}
   451  
   452  			err = checkPermissionMixedConfig(apiConfig)
   453  			if err != nil {
   454  				return nil, err
   455  			}
   456  
   457  			apiConfig.CatchAllEndpoint = newCatchAllEndpoint()
   458  
   459  			apiErrorHandler := collect.ErrorHandlerSetters(apiConfig)
   460  			if err = configureErrorHandler(apiErrorHandler, helper); err != nil {
   461  				return nil, err
   462  			}
   463  		}
   464  
   465  		// standalone endpoints
   466  		err = refineEndpoints(helper, serverConfig.Endpoints, true, definedACs)
   467  		if err != nil {
   468  			return nil, err
   469  		}
   470  
   471  		helper.config.Servers = append(helper.config.Servers, serverConfig)
   472  	}
   473  
   474  	for _, job := range helper.config.Definitions.Job {
   475  		attrs := job.Remain.(*hclsyntax.Body).Attributes
   476  		r := attrs["interval"].Expr.Range()
   477  
   478  		job.IntervalDuration, err = config.ParseDuration("interval", job.Interval, -1)
   479  		if err != nil {
   480  			return nil, newDiagErr(&r, err.Error())
   481  		} else if job.IntervalDuration == -1 {
   482  			return nil, newDiagErr(&r, "invalid duration")
   483  		}
   484  
   485  		endpointConf := &config.Endpoint{
   486  			Pattern:  job.Name, // for error messages
   487  			Remain:   job.Remain,
   488  			Requests: job.Requests,
   489  		}
   490  
   491  		err = refineEndpoints(helper, config.Endpoints{endpointConf}, false, nil)
   492  		if err != nil {
   493  			return nil, err
   494  		}
   495  
   496  		job.Endpoint = endpointConf
   497  	}
   498  
   499  	if len(helper.config.Servers) == 0 {
   500  		return nil, fmt.Errorf("configuration error: missing 'server' block")
   501  	}
   502  
   503  	return helper.config, nil
   504  }
   505  
   506  // checkPermissionMixedConfig checks whether, for api blocks with at least two endpoints,
   507  // all endpoints in api have either
   508  // a) no required permission set or
   509  // b) required permission or disable_access_control set
   510  func checkPermissionMixedConfig(apiConfig *config.API) error {
   511  	if apiConfig.RequiredPermission != nil {
   512  		// default for required permission: no mixed config
   513  		return nil
   514  	}
   515  
   516  	l := len(apiConfig.Endpoints)
   517  	if l < 2 {
   518  		// too few endpoints: no mixed config
   519  		return nil
   520  	}
   521  
   522  	countEpsWithPermission := 0
   523  	countEpsWithPermissionOrDisableAC := 0
   524  	for _, e := range apiConfig.Endpoints {
   525  		if e.RequiredPermission != nil {
   526  			// endpoint has required permission attribute set
   527  			countEpsWithPermission++
   528  			countEpsWithPermissionOrDisableAC++
   529  		} else if e.DisableAccessControl != nil {
   530  			// endpoint has didable AC attribute set
   531  			countEpsWithPermissionOrDisableAC++
   532  		}
   533  	}
   534  
   535  	if countEpsWithPermission == 0 {
   536  		// no endpoints with required permission: no mixed config
   537  		return nil
   538  	}
   539  
   540  	if l > countEpsWithPermissionOrDisableAC {
   541  		return errors.Configuration.Messagef("api with label %q has endpoint without required permission", apiConfig.Name)
   542  	}
   543  
   544  	return nil
   545  }
   546  
   547  func absolutizePaths(fileBody *hclsyntax.Body) error {
   548  	visitor := func(node hclsyntax.Node) hcl.Diagnostics {
   549  		attribute, ok := node.(*hclsyntax.Attribute)
   550  		if !ok {
   551  			return nil
   552  		}
   553  
   554  		_, exists := pathBearingAttributesMap[attribute.Name]
   555  		if !exists {
   556  			return nil
   557  		}
   558  
   559  		value, diags := attribute.Expr.Value(envContext)
   560  		if diags.HasErrors() {
   561  			return diags
   562  		}
   563  
   564  		filePath := value.AsString()
   565  		basePath := attribute.SrcRange.Filename
   566  		var absolutePath string
   567  		if attribute.Name == "jwks_url" {
   568  			if strings.HasPrefix(filePath, "http://") || strings.HasPrefix(filePath, "https://") {
   569  				return nil
   570  			}
   571  
   572  			filePath = strings.TrimPrefix(filePath, "file:")
   573  			if path.IsAbs(filePath) {
   574  				return nil
   575  			}
   576  
   577  			absolutePath = "file:" + filepath.ToSlash(path.Join(filepath.Dir(basePath), filePath))
   578  		} else {
   579  			if filepath.IsAbs(filePath) {
   580  				return nil
   581  			}
   582  			absolutePath = filepath.Join(filepath.Dir(basePath), filePath)
   583  		}
   584  
   585  		attribute.Expr = &hclsyntax.LiteralValueExpr{
   586  			Val:      cty.StringVal(absolutePath),
   587  			SrcRange: attribute.SrcRange,
   588  		}
   589  
   590  		return nil
   591  	}
   592  
   593  	diags := hclsyntax.VisitAll(fileBody, visitor)
   594  	if diags.HasErrors() {
   595  		return diags
   596  	}
   597  	return nil
   598  }