Repository: avenga/couper
Pull request #504 (2022-05-11 13:02:37)

Evaluate required permission
Describe your changes!

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adad0df3..5f1c13f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,8 +18,8 @@ Unreleased changes are available as `avenga/couper:edge` container.
   * [Error types](./docs/ERRORS.md) `endpoint` and `access_control` ([#500](https://github.com/avenga/couper/pull/500))
 
 * **Changed**
-  * Permission handling: ([#477](https://github.com/avenga/couper/pull/477))
-    * renamed `beta_scope` attribute for [`api`](./docs/REFERENCE.md#api-block) and [`endpoint`](./docs/REFERENCE.md#endpoint-block) blocks to `beta_required_permission`; `beta_required_permission` in `endpoint` now *overriding* `beta_required_permission` in containing `api` block
+  * Permission handling: ([#477](https://github.com/avenga/couper/pull/477), [#504](https://github.com/avenga/couper/pull/504))
+    * renamed `beta_scope` attribute for [`api`](./docs/REFERENCE.md#api-block) and [`endpoint`](./docs/REFERENCE.md#endpoint-block) blocks to `beta_required_permission`; `beta_required_permission` in `endpoint` now *overriding* `beta_required_permission` in containing `api` block; allowing an expression as attribute value
     * renamed `beta_scope_claim` and `beta_scope_map` attributes for [`jwt` block](./docs/REFERENCE.md#jwt-block) to `beta_permissions_claim` and `beta_permissions_map`
     * removed `beta_operation_denied` and `beta_scope` [error types](./docs/ERRORS.md#api-and-endpoint-error-types)
     * renamed `beta_insufficient_scope` [error type](./docs/ERRORS.md#api-and-endpoint-error-types) to `beta_insufficient_permissions`
diff --git a/accesscontrol/permissions.go b/accesscontrol/permissions.go
index f9c828a6..6c566014 100644
--- a/accesscontrol/permissions.go
+++ b/accesscontrol/permissions.go
@@ -4,10 +4,13 @@ import (
 	"context"
 	"net/http"
 
+	"github.com/hashicorp/hcl/v2"
+
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler/middleware"
+	"github.com/avenga/couper/internal/seetie"
 )
 
 type requiredPermissions struct {
@@ -56,17 +59,26 @@ func (r *requiredPermissions) getPermission(method string) (string, error) {
 var _ AccessControl = &PermissionsControl{}
 
 type PermissionsControl struct {
-	required requiredPermissions
+	permissionExpr hcl.Expression
 }
 
-func NewPermissionsControl(permission string, permissionMap map[string]string) *PermissionsControl {
-	rp := newRequiredPermissions(permission, permissionMap)
-	return &PermissionsControl{required: rp}
+func NewPermissionsControl(permissionExpr hcl.Expression) *PermissionsControl {
+	return &PermissionsControl{permissionExpr: permissionExpr}
 }
 
 // Validate validates the granted permissions provided by access controls against the required permission.
 func (p *PermissionsControl) Validate(req *http.Request) error {
-	requiredPermission, err := p.required.getPermission(req.Method)
+	if p.permissionExpr == nil {
+		return nil
+	}
+	permissionVal, err := eval.Value(eval.ContextFromRequest(req).HCLContext(), p.permissionExpr)
+	permission, permissionMap, err := seetie.ValueToPermission(permissionVal)
+	if err != nil {
+		return errors.Evaluation.With(err)
+	}
+
+	rp := newRequiredPermissions(permission, permissionMap)
+	requiredPermission, err := rp.getPermission(req.Method)
 	if err != nil {
 		return err
 	}
diff --git a/config/api.go b/config/api.go
index ac07d505..dbdf7136 100644
--- a/config/api.go
+++ b/config/api.go
@@ -3,7 +3,6 @@ package config
 import (
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
-	"github.com/zclconf/go-cty/cty"
 )
 
 var _ Inline = &API{}
@@ -20,10 +19,10 @@ type API struct {
 	ErrorFile            string    `hcl:"error_file,optional"`
 	Name                 string    `hcl:"name,label"`
 	Remain               hcl.Body  `hcl:",remain"`
-	RequiredPermission   cty.Value `hcl:"beta_required_permission,optional"`
 
 	// internally used
-	CatchAllEndpoint *Endpoint
+	CatchAllEndpoint   *Endpoint
+	RequiredPermission hcl.Expression
 }
 
 // APIs represents a list of <API> objects.
@@ -41,6 +40,7 @@ func (a API) Inline() interface{} {
 		DelResponseHeaders []string                  `hcl:"remove_response_headers,optional"`
 		SetResponseHeaders map[string]string         `hcl:"set_response_headers,optional"`
 		LogFields          map[string]hcl.Expression `hcl:"custom_log_fields,optional"`
+		RequiredPermission hcl.Expression            `hcl:"beta_required_permission,optional"`
 	}
 
 	return &Inline{}
diff --git a/config/configload/endpoint.go b/config/configload/endpoint.go
index f798e449..c6160f6c 100644
--- a/config/configload/endpoint.go
+++ b/config/configload/endpoint.go
@@ -50,6 +50,11 @@ func refineEndpoints(helper *helper, endpoints config.Endpoints, check bool) err
 
 		endpointContent := bodyToContent(endpoint.Remain)
 
+		rp := endpointContent.Attributes["beta_required_permission"]
+		if rp != nil {
+			endpoint.RequiredPermission = rp.Expr
+		}
+
 		if check && endpoint.AllowedMethods != nil && len(endpoint.AllowedMethods) > 0 {
 			if err = validMethods(endpoint.AllowedMethods, &endpointContent.Attributes["allowed_methods"].Range); err != nil {
 				return err
diff --git a/config/configload/load.go b/config/configload/load.go
index 67460c1a..67b4dcc9 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -11,7 +11,6 @@ import (
 	"github.com/hashicorp/hcl/v2/gohcl"
 	"github.com/hashicorp/hcl/v2/hclparse"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
-	"github.com/zclconf/go-cty/cty"
 
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/configload/collect"
@@ -365,18 +364,25 @@ func LoadConfig(body hcl.Body, src []byte, filename, dirPath string) (*config.Co
 				apiConfig.Name = apiBlock.Labels[0]
 			}
 
+			apiContent := bodyToContent(apiConfig.Remain)
+
 			if apiConfig.AllowedMethods != nil && len(apiConfig.AllowedMethods) > 0 {
-				if err = validMethods(apiConfig.AllowedMethods, &bodyToContent(apiConfig.Remain).Attributes["allowed_methods"].Range); err != nil {
+				if err = validMethods(apiConfig.AllowedMethods, &apiContent.Attributes["allowed_methods"].Range); err != nil {
 					return nil, err
 				}
 			}
 
-			err = checkPermissionMixedConfig(apiConfig)
+			rp := apiContent.Attributes["beta_required_permission"]
+			if rp != nil {
+				apiConfig.RequiredPermission = rp.Expr
+			}
+
+			err = refineEndpoints(helper, apiConfig.Endpoints, true)
 			if err != nil {
 				return nil, err
 			}
 
-			err = refineEndpoints(helper, apiConfig.Endpoints, true)
+			err = checkPermissionMixedConfig(apiConfig)
 			if err != nil {
 				return nil, err
 			}
@@ -411,7 +417,7 @@ func LoadConfig(body hcl.Body, src []byte, filename, dirPath string) (*config.Co
 // a) no required permission set or
 // b) required permission or disable_access_control set
 func checkPermissionMixedConfig(apiConfig *config.API) error {
-	if apiConfig.RequiredPermission != cty.NilVal {
+	if apiConfig.RequiredPermission != nil {
 		// default for required permission: no mixed config
 		return nil
 	}
@@ -425,7 +431,7 @@ func checkPermissionMixedConfig(apiConfig *config.API) error {
 	countEpsWithPermission := 0
 	countEpsWithPermissionOrDisableAC := 0
 	for _, e := range apiConfig.Endpoints {
-		if e.RequiredPermission != cty.NilVal {
+		if e.RequiredPermission != nil {
 			// endpoint has required permission attribute set
 			countEpsWithPermission++
 			countEpsWithPermissionOrDisableAC++
diff --git a/config/endpoint.go b/config/endpoint.go
index 5ca20054..24cfbcfc 100644
--- a/config/endpoint.go
+++ b/config/endpoint.go
@@ -3,7 +3,6 @@ package config
 import (
 	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/gohcl"
-	"github.com/zclconf/go-cty/cty"
 
 	"github.com/avenga/couper/config/meta"
 )
@@ -21,12 +20,12 @@ type Endpoint struct {
 	Remain               hcl.Body  `hcl:",remain"`
 	RequestBodyLimit     string    `hcl:"request_body_limit,optional"`
 	Response             *Response `hcl:"response,block"`
-	RequiredPermission   cty.Value `hcl:"beta_required_permission,optional"`
 
 	// internally configured due to multi-label options
-	Proxies   Proxies
-	Requests  Requests
-	Sequences Sequences
+	Proxies            Proxies
+	Requests           Requests
+	RequiredPermission hcl.Expression
+	Sequences          Sequences
 }
 
 // Endpoints represents a list of <Endpoint> objects.
@@ -41,10 +40,11 @@ func (e Endpoint) HCLBody() hcl.Body {
 func (e Endpoint) Inline() interface{} {
 	type Inline struct {
 		meta.Attributes
-		Proxies        Proxies                   `hcl:"proxy,block"`
-		Requests       Requests                  `hcl:"request,block"`
-		ResponseStatus *uint8                    `hcl:"set_response_status,optional"`
-		LogFields      map[string]hcl.Expression `hcl:"custom_log_fields,optional"`
+		Proxies            Proxies                   `hcl:"proxy,block"`
+		Requests           Requests                  `hcl:"request,block"`
+		ResponseStatus     *uint8                    `hcl:"set_response_status,optional"`
+		LogFields          map[string]hcl.Expression `hcl:"custom_log_fields,optional"`
+		RequiredPermission hcl.Expression            `hcl:"beta_required_permission,optional"`
 	}
 
 	return &Inline{}
diff --git a/config/runtime/server.go b/config/runtime/server.go
index f7b34344..064c9f14 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -14,7 +14,6 @@ import (
 	"github.com/docker/go-units"
 	"github.com/hashicorp/hcl/v2"
 	"github.com/sirupsen/logrus"
-	"github.com/zclconf/go-cty/cty"
 
 	ac "github.com/avenga/couper/accesscontrol"
 	"github.com/avenga/couper/accesscontrol/jwk"
@@ -28,7 +27,6 @@ import (
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
 	"github.com/avenga/couper/handler/middleware"
-	"github.com/avenga/couper/internal/seetie"
 	"github.com/avenga/couper/oauth2"
 	"github.com/avenga/couper/oauth2/oidc"
 	"github.com/avenga/couper/utils"
@@ -305,19 +303,15 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				}
 				epHandler = handler.NewEndpoint(epOpts, log, modifier)
 
-				requiredPermissionVal := endpointConf.RequiredPermission
-				if requiredPermissionVal == cty.NilVal && parentAPI != nil {
+				requiredPermissionExpr := endpointConf.RequiredPermission
+				if requiredPermissionExpr == nil && parentAPI != nil {
 					// if required permission in endpoint {} not defined, try required permission in parent api {}
-					requiredPermissionVal = parentAPI.RequiredPermission
+					requiredPermissionExpr = parentAPI.RequiredPermission
 				}
-				if requiredPermissionVal == cty.NilVal {
+				if requiredPermissionExpr == nil {
 					protectedHandler = epHandler
 				} else {
-					requiredPermission, requiredPermissionMap, err := seetie.ValueToPermission(requiredPermissionVal)
-					if err != nil {
-						return nil, err
-					}
-					permissionsControl := ac.NewPermissionsControl(requiredPermission, requiredPermissionMap)
+					permissionsControl := ac.NewPermissionsControl(requiredPermissionExpr)
 					permissionsErrorHandler, err := newErrorHandler(confCtx, &protectedOptions{
 						epOpts:   epOpts,
 						memStore: memStore,
diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index 903c296f..4aa5b03c 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -121,7 +121,7 @@ as JSON error with an error body payload. This can be customized via `error_file
 | `access_control` |tuple (string)|-|Sets predefined [Access Control](#access-control) for `api` block context.|&#9888; Inherited by nested blocks.| `access_control = ["foo"]`|
 | `disable_access_control` | tuple (string) | - | Disables access controls by name. | - | `disable_access_control = ["foo"]` |
 | `allowed_methods` | tuple (string) | `["*"]` == `["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]` | Sets allowed methods as _default_ for all contained endpoints. Requests with a method that is not allowed result in an error response with a `405 Method Not Allowed` status. | The default value `*` can be combined with additional methods. Methods are matched case-insensitively. `Access-Control-Allow-Methods` is only sent in response to a [CORS](#cors-block) preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method. | `allowed_methods = ["GET", "POST"]` or `allowed_methods = ["*", "BREW"]` |
-| `beta_required_permission` |string or object (string)|-|Permission required to use this API (see [error type](ERRORS.md#error-types) `beta_insufficient_permissions`).|If the value is a string, the same permission applies to all request methods. If there are different permissions for different request methods, use an object with the request methods as keys and string values. Methods not specified in this object are not permitted. `"*"` is the key for "all other standard methods". Methods other than `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` must be specified explicitly. A value `""` means "no permission required".| `beta_required_permission = "read"` or `beta_required_permission = { post = "write", "*" = "" }`|
+| `beta_required_permission` | expression evaluating to string or object (string)|-|Permission required to use this API (see [error type](ERRORS.md#error-types) `beta_insufficient_permissions`).|If the value is a string, the same permission applies to all request methods. If there are different permissions for different request methods, use an object with the request methods as keys and string values. Methods not specified in this object are not permitted. `"*"` is the key for "all other standard methods". Methods other than `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` must be specified explicitly. A value `""` means "no permission required".| `beta_required_permission = "read"` or `beta_required_permission = { post = "write", "*" = "" }` or `beta_required_permission = default(request.path_params.p, "not_set")`|
 | `custom_log_fields` | object | - | Defines log fields for [Custom Logging](LOGS.md#custom-logging). | &#9888; Inherited by nested blocks. | - |
 
 ### Endpoint Block
@@ -145,7 +145,7 @@ produce an explicit or implicit client response.
 | `access_control`        | tuple (string)   | -       | Sets predefined [Access Control](#access-control) for `endpoint` block context.                                   | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | `access_control = ["foo"]`                                           |
 | `disable_access_control` | tuple (string)  | -       | Disables access controls by name.                                                                                 | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | `disable_access_control = ["foo"]`                                   |
 | `allowed_methods`       | tuple (string)  | `["*"]` == `["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]` | Sets allowed methods _overriding_ a default set in the containing `api` block. Requests with a method that is not allowed result in an error response with a `405 Method Not Allowed` status. | The default value `*` can be combined with additional methods. Methods are matched case-insensitively. `Access-Control-Allow-Methods` is only sent in response to a [CORS](#cors-block) preflight request, if the method requested by `Access-Control-Request-Method` is an allowed method. | `allowed_methods = ["GET", "POST"]` or `allowed_methods = ["*", "BREW"]` |
-| `beta_required_permission` | string or object (string) | -    | Permission required to use this endpoint (see [error type](ERRORS.md#error-types) `beta_insufficient_permissions`).| Overrides `beta_required_permission` in a containing `api` block. If the value is a string, the same permission applies to all request methods. If there are different permissions for different request methods, use an object with the request methods as keys and string values. Methods not specified in this object are not permitted. `"*"` is the key for "all other standard methods". Methods other than `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` must be specified explicitly. A value `""` means "no permission required". For `api` blocks with at least two `endpoint`s, all endpoints must have either a) no `beta_required_permission` set or b) either `beta_required_permission` or `disable_access_control` set. Otherwise, a configuration error is thrown. | `beta_required_permission = "read"` or `beta_required_permission = { post = "write", "*" = "" }` |
+| `beta_required_permission` | expression evaluating to string or object (string) | -    | Permission required to use this endpoint (see [error type](ERRORS.md#error-types) `beta_insufficient_permissions`).| Overrides `beta_required_permission` in a containing `api` block. If the value is a string, the same permission applies to all request methods. If there are different permissions for different request methods, use an object with the request methods as keys and string values. Methods not specified in this object are not permitted. `"*"` is the key for "all other standard methods". Methods other than `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` must be specified explicitly. A value `""` means "no permission required". For `api` blocks with at least two `endpoint`s, all endpoints must have either a) no `beta_required_permission` set or b) either `beta_required_permission` or `disable_access_control` set. Otherwise, a configuration error is thrown. | `beta_required_permission = "read"` or `beta_required_permission = { post = "write", "*" = "" }` or `beta_required_permission = default(request.path_params.p, "not_set")` |
 | `custom_log_fields`     | object           | -       | Defines log fields for [Custom Logging](LOGS.md#custom-logging).                                                  | &#9888; Inherited by nested blocks.                                                                                                                                                                                                                                                                                                                                                                                                             | -                                                                    |
 | [Modifiers](#modifiers) | -                | -       | -                                                                                                                 | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | -                                                                    |
 


Test patch:
diff --git a/accesscontrol/permissions_test.go b/accesscontrol/permissions_test.go
index 32aa9c05..d6bfe6ae 100644
--- a/accesscontrol/permissions_test.go
+++ b/accesscontrol/permissions_test.go
@@ -6,6 +6,10 @@ import (
 	"net/http/httptest"
 	"testing"
 
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/zclconf/go-cty/cty"
+
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/errors"
 )
@@ -94,6 +98,30 @@ func Test_PermissionsControl(t *testing.T) {
 			nil,
 			"",
 		},
+		{
+			"no method restrictions, no permission required, permission granted",
+			"",
+			nil,
+			http.MethodGet,
+			[]string{"read"},
+			"",
+		},
+		{
+			"no method permitted, no permission granted",
+			"",
+			map[string]string{},
+			http.MethodGet,
+			nil,
+			"method not allowed error: method GET not allowed by beta_required_permission",
+		},
+		{
+			"no method permitted, permission granted",
+			"",
+			map[string]string{},
+			http.MethodPost,
+			[]string{"read"},
+			"method not allowed error: method POST not allowed by beta_required_permission",
+		},
 		{
 			"method permitted, no permission required, no permission granted",
 			"",
@@ -126,6 +154,22 @@ func Test_PermissionsControl(t *testing.T) {
 			[]string{"read"},
 			"",
 		},
+		{
+			"method permitted, permission required, permission granted",
+			"",
+			map[string]string{"Brew": "tea"},
+			"BREW",
+			[]string{"tea"},
+			"",
+		},
+		{
+			"method permitted, permission required, missing required permission",
+			"",
+			map[string]string{"Brew": "tea"},
+			"BREW",
+			[]string{"coffee"},
+			`access control error: required permission "tea" not granted`,
+		},
 		{
 			"default methods permitted, permission required, permission granted",
 			"",
@@ -170,7 +214,45 @@ func Test_PermissionsControl(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(subT *testing.T) {
-			pc := NewPermissionsControl(tt.permission, tt.permissionMap)
+			var expr hcl.Expression
+			if tt.permission != "" {
+				expr = &hclsyntax.TemplateExpr{
+					Parts: []hclsyntax.Expression{
+						&hclsyntax.LiteralValueExpr{
+							Val: cty.StringVal(tt.permission),
+						},
+					},
+				}
+			} else if tt.permissionMap != nil {
+				var items []hclsyntax.ObjectConsItem
+				for k, v := range tt.permissionMap {
+					item := hclsyntax.ObjectConsItem{
+						KeyExpr: &hclsyntax.ObjectConsKeyExpr{
+							Wrapped: &hclsyntax.TemplateExpr{
+								Parts: []hclsyntax.Expression{
+									&hclsyntax.LiteralValueExpr{
+										Val: cty.StringVal(k),
+									},
+								},
+							},
+						},
+						ValueExpr: &hclsyntax.TemplateExpr{
+							Parts: []hclsyntax.Expression{
+								&hclsyntax.LiteralValueExpr{
+									Val: cty.StringVal(v),
+								},
+							},
+						},
+					}
+					items = append(items, item)
+				}
+				expr = &hclsyntax.ObjectConsExpr{
+					Items: items,
+				}
+			} else {
+				expr = nil
+			}
+			pc := NewPermissionsControl(expr)
 			req := httptest.NewRequest(tt.method, "/", nil)
 			if tt.grantedPermissions != nil {
 				ctx := req.Context()
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 0296814c..3215f1e6 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -3788,6 +3788,14 @@ func Test_Permissions(t *testing.T) {
 		{"by scope: method not permitted", http.MethodDelete, "/scope/foo", true, http.StatusMethodNotAllowed, ``, ``, "method not allowed error: method DELETE not allowed by beta_required_permission", ""},
 		{"by scope: permission required by endpoint via *: insufficient permissions", http.MethodGet, "/scope/bar", true, http.StatusForbidden, ``, `more`, `access control error: required permission "more" not granted`, "beta_insufficient_permissions"},
 		{"by scope: no permission required by endpoint", http.MethodDelete, "/scope/bar", true, http.StatusNoContent, `["a"]`, ``, "", ""},
+		{"by scope: required permission expression", http.MethodGet, "/scope/path/a/path", true, http.StatusNoContent, `["a"]`, ``, "", ""},
+		{"by scope: required permission object expression (GET)", http.MethodGet, "/scope/object/get", true, http.StatusNoContent, `["a"]`, ``, "", ""},
+		{"by scope: required permission object expression (DELETE)", http.MethodDelete, "/scope/object/delete", true, http.StatusForbidden, ``, ``, `access control error: required permission "z" not granted`, "beta_insufficient_permissions"},
+		{"by scope: required permission bad expression", http.MethodGet, "/scope/bad/expression", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
+		{"by scope: required permission bad type number", http.MethodGet, "/scope/bad/type/number", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
+		{"by scope: required permission bad type boolean", http.MethodGet, "/scope/bad/type/boolean", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
+		{"by scope: required permission bad type tuple", http.MethodGet, "/scope/bad/type/tuple", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
+		{"by scope: required permission bad type null", http.MethodGet, "/scope/bad/type/null", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"},
 		{"by scope: required permission by api only: insufficient permissions", http.MethodGet, "/scope/permission-from-api", true, http.StatusForbidden, ``, ``, `access control error: required permission "z" not granted`, "beta_insufficient_permissions"},
 		{"by role: unauthorized", http.MethodGet, "/role/foo", false, http.StatusUnauthorized, ``, ``, "access control error: roled_jwt: token required", "jwt_token_missing"},
 		{"by role: sufficient permission", http.MethodGet, "/role/foo", true, http.StatusNoContent, `["a","b"]`, ``, "", ""},
diff --git a/server/testdata/integration/config/09_couper.hcl b/server/testdata/integration/config/09_couper.hcl
index 449c58d3..a99bb488 100644
--- a/server/testdata/integration/config/09_couper.hcl
+++ b/server/testdata/integration/config/09_couper.hcl
@@ -35,6 +35,71 @@ server "scoped jwt" {
         }
       }
     }
+    endpoint "/path/{p}/path" {
+      beta_required_permission = request.path_params.p
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
+    endpoint "/object/{method}" {
+      beta_required_permission = {
+        (request.path_params.method) = contains(["get", "post"], request.path_params.method) ? "a" : "z"
+      }
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
+    endpoint "/bad/expression" {
+      beta_required_permission = request
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
+    endpoint "/bad/type/number" {
+      beta_required_permission = 123
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
+    endpoint "/bad/type/boolean" {
+      beta_required_permission = true
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
+    endpoint "/bad/type/tuple" {
+      beta_required_permission = ["p1", "p2"]
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
+    endpoint "/bad/type/null" {
+      beta_required_permission = null
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
     endpoint "/permission-from-api" {
       response {
         status = 204