Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ docs/website/.vite
/*.key

vendor
/.run/
10 changes: 6 additions & 4 deletions accesscontrol/ac.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package accesscontrol

import (
"errors"
"net/http"

"github.com/coupergateway/couper/errors"
couperErrors "github.com/coupergateway/couper/errors"
"github.com/coupergateway/couper/eval"
)

Expand All @@ -24,7 +25,7 @@ func NewItem(nameLabel string, control AccessControl, errHandler http.Handler) *
return &ListItem{
control: control,
controlErrHandler: errHandler,
kind: errors.TypeToSnake(control),
kind: couperErrors.TypeToSnake(control),
label: nameLabel,
}
}
Expand All @@ -45,10 +46,11 @@ var _ AccessControl = ValidateFunc(func(_ *http.Request) error { return nil })

func (i ListItem) Validate(req *http.Request) error {
if err := i.control.Validate(req); err != nil {
if e, ok := err.(*errors.Error); ok {
var e *couperErrors.Error
if errors.As(err, &e) {
return e.Label(i.label)
}
return errors.AccessControl.Label(i.label).Kind(i.kind).With(err)
return couperErrors.AccessControl.Label(i.label).Kind(i.kind).With(err)
}

evalCtx := eval.ContextFromRequest(req)
Expand Down
53 changes: 53 additions & 0 deletions accesscontrol/authz/external.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package authz

import (
"net/http"

"github.com/coupergateway/couper/errors"
)

type DestinationRoundTripper interface {
http.RoundTripper
GetDestination() string
}

// External authorization calls the configured origin with customized request context.
// The origin must respond with 200 OK to have a valid client request.
type External struct {
origin DestinationRoundTripper
includeMetadataTLS bool
// TODO
// conf for who to what
// params, header or body or both
// pass certificate
}

type clientRequest struct {
Method string
URL string
Header http.Header
}

type authContext struct {
Source any // previous hop
Destination any // target backend (origin)
ClientRequest clientRequest // simplified form / serialized
Route any
Metadata any // user / hcl provided
MetadataTLS any // tls conn infos / opt in
}

Comment on lines +25 to +39
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clientRequest and authContext types are defined but never used in the code. These should either be utilized in the implementation or removed to avoid dead code.

Suggested change
type clientRequest struct {
Method string
URL string
Header http.Header
}
type authContext struct {
Source any // previous hop
Destination any // target backend (origin)
ClientRequest clientRequest // simplified form / serialized
Route any
Metadata any // user / hcl provided
MetadataTLS any // tls conn infos / opt in
}

Copilot uses AI. Check for mistakes.
func NewExternal(origin DestinationRoundTripper, includeMetadataTLS bool) (*External, error) {
return &External{
origin: origin,
includeMetadataTLS: includeMetadataTLS,
}, nil
}
Comment on lines +40 to +45
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NewExternal function always returns nil error but validates that origin is required in the Validate method. If origin is a required parameter, validation should occur during construction in NewExternal to fail fast, or the function signature should not return an error if it never fails.

Copilot uses AI. Check for mistakes.

func (c *External) Validate(req *http.Request) error {
if c.origin == nil {
return errors.AccessControl.Message("origin required")
}
//TODO implement me
panic("implement me")
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Validate method contains a panic statement that will crash the application if called. This should either be removed if this is work-in-progress code, or replaced with a proper error return until implementation is complete.

Suggested change
panic("implement me")
return errors.AccessControl.Message("not implemented")

Copilot uses AI. Check for mistakes.
}
48 changes: 48 additions & 0 deletions config/ac_authz_external.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package config

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)

type OpenFGAEntity struct {
Namespace string `hcl:"namespace" docs:"The namespace of the entity."`
Name hcl.Expression `hcl:"name" docs:"The name or identifier of the entity."`
}

type OpenFGAEntityRelation struct {
Name hcl.Expression `hcl:"name" docs:"The name of the relation."`
}

type AuthZOpenFGA struct {
User *OpenFGAEntity `hcl:"user,block" docs:"The user entity."`
Relation *OpenFGAEntityRelation `hcl:"relation,block" docs:"The relation name."`
Object *OpenFGAEntity `hcl:"object,block" docs:"The object entity."`
StoreID string `hcl:"store_id" docs:"The store ID to use for authorization against the OpenFGA server."`
ModelID string `hcl:"model_id,optional" docs:"The model ID to use for authorization against the OpenFGA store. If omitted, the latest store model is used."`
Remain hcl.Body `hcl:",remain"`
}

type AuthZExternal struct {
BackendName string `hcl:"backend" docs:"References a default [backend](/configuration/block/backend) in [definitions](/configuration/block/definitions) for authZ requests. Mutually exclusive with {backend} block."`
URL string `hcl:"url,optional" docs:"The URL to call for authorization."`
IncludeTLS bool `hcl:"include_tls,optional" docs:"Include TLS information in the authorization request."`
Name string `hcl:"name,label" docs:"The name of the authorization."`
OpenFGA *AuthZOpenFGA `hcl:"openfga,block" docs:"Configure an [OpenFGA](/configuration/block/authz_external/openfga) authorization."`
Remain hcl.Body `hcl:",remain"`

// Internally used
Backend *hclsyntax.Body
}

func (a *AuthZExternal) Prepare(backendFunc PrepareBackendFunc) (err error) {
if a.URL != "" {
a.Backend, err = backendFunc(a.BackendName, a.Name, a)
return err
}
return nil
}

func (a *AuthZExternal) HCLBody() *hclsyntax.Body {
return a.Remain.(*hclsyntax.Body)
}
35 changes: 19 additions & 16 deletions config/configload/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ func (h *helper) configureDefinedBackends() error {

func (h *helper) configureACBackends() error {
var acs []config.BackendInitialization

for _, ac := range h.config.Definitions.AuthZExternal {
acs = append(acs, ac)
}

for _, ac := range h.config.Definitions.JWT {
acs = append(acs, ac)
}
Expand Down Expand Up @@ -207,8 +212,7 @@ func (h *helper) configureJWTSigningConfig() (map[string]*lib.JWTSigningConfig,
}

// Reads per server block and merge backend settings which results in a final server configuration.
func (h *helper) configureServers(body *hclsyntax.Body) error {
var err error
func (h *helper) configureServers(body *hclsyntax.Body) (err error) {
defsACs := h.getDefinedACs()

for _, serverBlock := range hclbody.BlocksOfType(body, server) {
Expand All @@ -227,13 +231,13 @@ func (h *helper) configureServers(body *hclsyntax.Body) error {
}

for _, fileConfig := range serverConfig.Files {
if err := checkReferencedAccessControls(fileConfig.HCLBody(), fileConfig.AccessControl, fileConfig.DisableAccessControl, defsACs); err != nil {
if err = checkReferencedAccessControls(fileConfig.HCLBody(), fileConfig.AccessControl, fileConfig.DisableAccessControl, defsACs); err != nil {
return err
}
}

for _, spaConfig := range serverConfig.SPAs {
if err := checkReferencedAccessControls(spaConfig.HCLBody(), spaConfig.AccessControl, spaConfig.DisableAccessControl, defsACs); err != nil {
if err = checkReferencedAccessControls(spaConfig.HCLBody(), spaConfig.AccessControl, spaConfig.DisableAccessControl, defsACs); err != nil {
return err
}
}
Expand All @@ -256,9 +260,7 @@ func (h *helper) configureServers(body *hclsyntax.Body) error {
}

// Reads api blocks and merge backends with server and definitions backends.
func (h *helper) configureAPIs(apis config.APIs, defsACs map[string]struct{}) error {
var err error

func (h *helper) configureAPIs(apis config.APIs, defsACs map[string]struct{}) (err error) {
for _, apiConfig := range apis {
apiBody := apiConfig.HCLBody()

Expand Down Expand Up @@ -298,9 +300,7 @@ func (h *helper) configureAPIs(apis config.APIs, defsACs map[string]struct{}) er
return nil
}

func (h *helper) configureJobs() error {
var err error

func (h *helper) configureJobs() (err error) {
for _, job := range h.config.Definitions.Job {
attrs := job.Remain.(*hclsyntax.Body).Attributes
r := attrs["interval"].Expr.Range()
Expand Down Expand Up @@ -456,22 +456,25 @@ func (h *helper) collectFromBlocks(authorizerBlocks hclsyntax.Blocks, name strin
}

func (h *helper) getDefinedACs() map[string]struct{} {
definitions := h.config.Definitions
definedACs := make(map[string]struct{})

for _, ac := range definitions.BasicAuth {
for _, ac := range h.config.Definitions.AuthZExternal {
definedACs[ac.Name] = struct{}{}
}
for _, ac := range definitions.JWT {

for _, ac := range h.config.Definitions.BasicAuth {
definedACs[ac.Name] = struct{}{}
}
for _, ac := range definitions.OAuth2AC {
for _, ac := range h.config.Definitions.JWT {
definedACs[ac.Name] = struct{}{}
}
for _, ac := range definitions.OIDC {
for _, ac := range h.config.Definitions.OAuth2AC {
definedACs[ac.Name] = struct{}{}
}
for _, ac := range h.config.Definitions.OIDC {
definedACs[ac.Name] = struct{}{}
}
for _, ac := range definitions.SAML {
for _, ac := range h.config.Definitions.SAML {
definedACs[ac.Name] = struct{}{}
}

Expand Down
30 changes: 15 additions & 15 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,59 +124,59 @@ func loadConfig(body *hclsyntax.Body) (*config.Couper, error) {
return nil, diags
}

helper, err := newHelper(body)
confHelper, err := newHelper(body)
if err != nil {
return nil, err
}

err = helper.configureBlocks()
err = confHelper.configureBlocks()
if err != nil {
return nil, err
}

e = helper.configureJWTSigningProfile()
e = confHelper.configureJWTSigningProfile()
if e != nil {
return nil, e
}

e = helper.configureSAML()
e = confHelper.configureSAML()
if e != nil {
return nil, e
}

jwtSigningConfigs, e := helper.configureJWTSigningConfig()
jwtSigningConfigs, e := confHelper.configureJWTSigningConfig()
if e != nil {
return nil, e
}

helper.config.Context = helper.config.Context.(*eval.Context).
confHelper.config.Context = confHelper.config.Context.(*eval.Context).
WithJWTSigningConfigs(jwtSigningConfigs).
WithOAuth2AC(helper.config.Definitions.OAuth2AC).
WithSAML(helper.config.Definitions.SAML)
WithOAuth2AC(confHelper.config.Definitions.OAuth2AC).
WithSAML(confHelper.config.Definitions.SAML)

err = helper.configureBindAddresses()
err = confHelper.configureBindAddresses()
if err != nil {
return nil, e
}

err = helper.configureServers(body)
err = confHelper.configureServers(body)
if err != nil {
return nil, err
}

err = helper.configureJobs()
err = confHelper.configureJobs()
if err != nil {
return nil, err
}

if len(helper.config.Servers) == 0 {
if len(confHelper.config.Servers) == 0 {
return nil, fmt.Errorf("configuration error: missing 'server' block")
}

return helper.config, nil
return confHelper.config, nil
}

func absolutizePaths(fileBody *hclsyntax.Body) ([]configfile.File, error) {
func resolveAbsolutePaths(fileBody *hclsyntax.Body) ([]configfile.File, error) {
const watchFilePrefix = "COUPER-WATCH-FILE: "

visitor := func(node hclsyntax.Node) hcl.Diagnostics {
Expand Down Expand Up @@ -293,7 +293,7 @@ func bodiesToConfig(parsedBodies []*hclsyntax.Body, srcBytes [][]byte, env strin
var watchFiles configfile.Files

for _, body := range parsedBodies {
files, err := absolutizePaths(body)
files, err := resolveAbsolutePaths(body)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions config/definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

// Definitions represents the <Definitions> object.
type Definitions struct {
AuthZExternal []*AuthZExternal `hcl:"authz_external,block" docs:"Configure a [authz_external](/configuration/block/authz_external) (zero or more)."`
Backend []*Backend `hcl:"backend,block" docs:"Configure a [backend](/configuration/block/backend) (zero or more)."`
BasicAuth []*BasicAuth `hcl:"basic_auth,block" docs:"Configure a [BasicAuth access control](/configuration/block/basic_auth) (zero or more)."`
Job []*Job `hcl:"beta_job,block" docs:"Configure a [job](/configuration/block/job) (zero or more)."`
Expand Down
23 changes: 17 additions & 6 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/sirupsen/logrus"

ac "github.com/coupergateway/couper/accesscontrol"
"github.com/coupergateway/couper/accesscontrol/authz"
"github.com/coupergateway/couper/accesscontrol/jwk"
"github.com/coupergateway/couper/cache"
"github.com/coupergateway/couper/config"
Expand Down Expand Up @@ -361,13 +362,13 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
protectedHandler = epHandler
} else {
permissionsControl := ac.NewPermissionsControl(requiredPermissionExpr)
permissionsErrorHandler, _, err := newErrorHandler(confCtx, conf, &protectedOptions{
permissionsErrorHandler, _, permErr := newErrorHandler(confCtx, conf, &protectedOptions{
epOpts: epOpts,
memStore: memStore,
srvOpts: serverOptions,
}, log, errorHandlerDefinitions, "api", "endpoint") // sequence of ref is important: api, endpoint (endpoint error_handler overrides api error_handler)
if err != nil {
return nil, err
if permErr != nil {
return nil, permErr
}

protectedHandler = middleware.NewErrorHandler(permissionsControl.Validate, permissionsErrorHandler)(epHandler)
Expand Down Expand Up @@ -510,12 +511,22 @@ func configureOidcConfigs(conf *config.Couper, confCtx *hcl.EvalContext, log *lo
return oidcConfigs, nil
}

func configureAccessControls(conf *config.Couper, confCtx *hcl.EvalContext, log *logrus.Entry,
memStore *cache.MemoryStore, oidcConfigs oidc.Configs) (ACDefinitions, error) {

func configureAccessControls(
conf *config.Couper, confCtx *hcl.EvalContext, log *logrus.Entry,
memStore *cache.MemoryStore, oidcConfigs oidc.Configs,
) (ACDefinitions, error) {
accessControls := make(ACDefinitions)

if conf.Definitions != nil {
for _, authZExternal := range conf.Definitions.AuthZExternal {
confErr := errors.Configuration.Label(authZExternal.Name)
authZExt, err := authz.NewExternal(nil, false)
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing nil for the origin parameter will cause Validate to always fail with 'origin required' error. The origin should be properly initialized from the backend configuration rather than passing nil.

Suggested change
authZExt, err := authz.NewExternal(nil, false)
authZExt, err := authz.NewExternal(authZExternal.Origin, false)

Copilot uses AI. Check for mistakes.
if err != nil {
return nil, confErr.With(err)
}
accessControls.Add(authZExternal.Name, authZExt, nil)
}

for _, baConf := range conf.Definitions.BasicAuth {
confErr := errors.Configuration.Label(baConf.Name)
basicAuth, err := ac.NewBasicAuth(baConf.Name, baConf.User, baConf.Pass, baConf.File)
Expand Down
Loading