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 }