Repository: avenga/couper
Pull request #477 (2022-04-08 12:27:22)

Permissions
Changed permission handling:
* renamed `beta_scope` attribute for `api` and `endpoint` blocks to `beta_required_permission`; `beta_required_permission` in `endpoint` now *overriding* `beta_required_permission` in containing `api` block
* renamed `beta_scope_claim` and `beta_scope_map` attributes for `jwt` block to `beta_permissions_claim` and `beta_permissions_map`
* removed `beta_operation_denied` and `beta_scope` error types
* renamed `beta_insufficient_scope` error type to `beta_insufficient_permissions`
* renamed undocumented `request.context.scopes` to `request.context.beta_granted_permissions`
* added `request.context.beta_required_permission`

```hcl
server {
  api {
    access_control = ["at"]

    error_handler "beta_insufficient_permissions" {
      response {
        status = 403
        json_body = {
          missing_permission = request.context.beta_required_permission
        }
      }
    }

    endpoint "/" {
      beta_required_permission = {
        GET = "calendar.readonly"
        POST = "calendar"
      }
      response {
        json_body = {
          granted_permissions = request.context.beta_granted_permissions
        }
      }
    }

    endpoint "/open" {
      beta_required_permission = ""
      response {
        body = "open"
      }
    }
  }
}

definitions {
  jwt "at" {
    beta_permissions_claim = "scope"
    beta_permissions_map = {
      calendar = ["calendar.readonly"]
    }
  }
}
```

---
<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 cc00bddf..4d1e8d1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,14 @@ Unreleased changes are available as `avenga/couper:edge` container.
   * `beta_health`-block to `backend`-block to enable continuous health-checks for defined backends ([#313](https://github.com/avenga/couper/pull/313))
     * `backends.<name>.health` variable to access the current health-check state _(subject to change)_
 
+* **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
+    * 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`
+    * added `request.context.beta_required_permission` and `request.context.beta_granted_permissions` [request variables](./docs/REFERENCE.md#request)
+
 * **Fixed**
   * Keys in object type attribute values are only handled case-insensitively if reasonable (e.g. they represent HTTP methods or header field values) ([#461](https://github.com/avenga/couper/pull/461))
   * Multiple labels for [`error_handler` blocks](./docs/ERRORS.md#error_handler-specification) ([#462](https://github.com/avenga/couper/pull/462))
diff --git a/accesscontrol/jwt.go b/accesscontrol/jwt.go
index 2d73dce7..c116ed1a 100644
--- a/accesscontrol/jwt.go
+++ b/accesscontrol/jwt.go
@@ -55,8 +55,8 @@ type JWT struct {
 	pubKey                interface{}
 	rolesClaim            string
 	rolesMap              map[string][]string
-	scopeClaim            string
-	scopeMap              map[string][]string
+	permissionsClaim      string
+	permissionsMap        map[string][]string
 	jwks                  *jwk.JWKS
 }
 
@@ -68,8 +68,8 @@ type JWTOptions struct {
 	Name                  string // TODO: more generic (validate)
 	RolesClaim            string
 	RolesMap              map[string][]string
-	ScopeClaim            string
-	ScopeMap              map[string][]string
+	PermissionsClaim      string
+	PermissionsMap        map[string][]string
 	Source                JWTSource
 	Key                   []byte
 	JWKS                  *jwk.JWKS
@@ -173,8 +173,8 @@ func newJWT(options *JWTOptions) (*JWT, error) {
 		name:                  options.Name,
 		rolesClaim:            options.RolesClaim,
 		rolesMap:              options.RolesMap,
-		scopeClaim:            options.ScopeClaim,
-		scopeMap:              options.ScopeMap,
+		permissionsClaim:      options.PermissionsClaim,
+		permissionsMap:        options.PermissionsMap,
 		source:                options.Source,
 	}
 	return jwtAC, nil
@@ -257,13 +257,13 @@ func (j *JWT) Validate(req *http.Request) error {
 	ctx = context.WithValue(ctx, request.AccessControls, acMap)
 
 	log := req.Context().Value(request.LogEntry).(*logrus.Entry).WithContext(req.Context())
-	scopesValues := j.getScopeValues(tokenClaims, log)
+	jwtGrantedPermissions := j.getGrantedPermissions(tokenClaims, log)
 
-	scopes, _ := ctx.Value(request.Scopes).([]string)
+	grantedPermissions, _ := ctx.Value(request.BetaGrantedPermissions).([]string)
 
-	scopes = append(scopes, scopesValues...)
+	grantedPermissions = append(grantedPermissions, jwtGrantedPermissions...)
 
-	ctx = context.WithValue(ctx, request.Scopes, scopes)
+	ctx = context.WithValue(ctx, request.BetaGrantedPermissions, grantedPermissions)
 
 	*req = *req.WithContext(ctx)
 
@@ -351,56 +351,56 @@ func (j *JWT) validateClaims(token *jwt.Token, claims map[string]interface{}) (m
 	return tokenClaims, nil
 }
 
-func (j *JWT) getScopeValues(tokenClaims map[string]interface{}, log *logrus.Entry) []string {
-	var scopeValues []string
+func (j *JWT) getGrantedPermissions(tokenClaims map[string]interface{}, log *logrus.Entry) []string {
+	var grantedPermissions []string
 
-	scopeValues = j.addScopeValueFromScope(tokenClaims, scopeValues, log)
+	grantedPermissions = j.addPermissionsFromPermissionsClaim(tokenClaims, grantedPermissions, log)
 
-	scopeValues = j.addScopeValueFromRoles(tokenClaims, scopeValues, log)
+	grantedPermissions = j.addPermissionsFromRoles(tokenClaims, grantedPermissions, log)
 
-	scopeValues = j.addMappedScopeValues(scopeValues, scopeValues)
+	grantedPermissions = j.addMappedPermissions(grantedPermissions, grantedPermissions)
 
-	return scopeValues
+	return grantedPermissions
 }
 
 const warnInvalidValueMsg = "invalid %s claim value type, ignoring claim, value %#v"
 
-func (j *JWT) addScopeValueFromScope(tokenClaims map[string]interface{}, scopeValues []string, log *logrus.Entry) []string {
-	if j.scopeClaim == "" {
-		return scopeValues
+func (j *JWT) addPermissionsFromPermissionsClaim(tokenClaims map[string]interface{}, permissions []string, log *logrus.Entry) []string {
+	if j.permissionsClaim == "" {
+		return permissions
 	}
 
-	scopesFromClaim, exists := tokenClaims[j.scopeClaim]
+	permissionsFromClaim, exists := tokenClaims[j.permissionsClaim]
 	if !exists {
-		return scopeValues
+		return permissions
 	}
 
 	// ["foo", "bar"] is stored as []interface{}, not []string, unfortunately
-	scopesArray, ok := scopesFromClaim.([]interface{})
+	permissionsArray, ok := permissionsFromClaim.([]interface{})
 	if ok {
 		var vals []string
-		for _, v := range scopesArray {
-			s, ok := v.(string)
+		for _, v := range permissionsArray {
+			p, ok := v.(string)
 			if !ok {
-				log.Warn(fmt.Sprintf(warnInvalidValueMsg, "scope", scopesFromClaim))
-				return scopeValues
+				log.Warn(fmt.Sprintf(warnInvalidValueMsg, "permissions", permissionsFromClaim))
+				return permissions
 			}
-			vals = append(vals, s)
+			vals = append(vals, p)
 		}
 		for _, val := range vals {
-			scopeValues, _ = addScopeValue(scopeValues, val)
+			permissions, _ = addPermission(permissions, val)
 		}
 	} else {
-		scopesString, ok := scopesFromClaim.(string)
+		permissionsString, ok := permissionsFromClaim.(string)
 		if !ok {
-			log.Warn(fmt.Sprintf(warnInvalidValueMsg, "scope", scopesFromClaim))
-			return scopeValues
+			log.Warn(fmt.Sprintf(warnInvalidValueMsg, "permissions", permissionsFromClaim))
+			return permissions
 		}
-		for _, s := range strings.Split(scopesString, " ") {
-			scopeValues, _ = addScopeValue(scopeValues, s)
+		for _, p := range strings.Split(permissionsString, " ") {
+			permissions, _ = addPermission(permissions, p)
 		}
 	}
-	return scopeValues
+	return permissions
 }
 
 func (j *JWT) getRoleValues(rolesClaimValue interface{}, log *logrus.Entry) []string {
@@ -428,40 +428,40 @@ func (j *JWT) getRoleValues(rolesClaimValue interface{}, log *logrus.Entry) []st
 	}
 }
 
-func (j *JWT) addScopeValueFromRoles(tokenClaims map[string]interface{}, scopeValues []string, log *logrus.Entry) []string {
+func (j *JWT) addPermissionsFromRoles(tokenClaims map[string]interface{}, permissions []string, log *logrus.Entry) []string {
 	if j.rolesClaim == "" || j.rolesMap == nil {
-		return scopeValues
+		return permissions
 	}
 
 	rolesClaimValue, exists := tokenClaims[j.rolesClaim]
 	if !exists {
-		return scopeValues
+		return permissions
 	}
 
 	roleValues := j.getRoleValues(rolesClaimValue, log)
 	for _, r := range roleValues {
-		if scopes, exist := j.rolesMap[r]; exist {
-			for _, s := range scopes {
-				scopeValues, _ = addScopeValue(scopeValues, s)
+		if perms, exist := j.rolesMap[r]; exist {
+			for _, p := range perms {
+				permissions, _ = addPermission(permissions, p)
 			}
 		}
 	}
 
-	if scopes, exist := j.rolesMap["*"]; exist {
-		for _, s := range scopes {
-			scopeValues, _ = addScopeValue(scopeValues, s)
+	if perms, exist := j.rolesMap["*"]; exist {
+		for _, p := range perms {
+			permissions, _ = addPermission(permissions, p)
 		}
 	}
-	return scopeValues
+	return permissions
 }
 
-func (j *JWT) addMappedScopeValues(source, target []string) []string {
-	if j.scopeMap == nil {
+func (j *JWT) addMappedPermissions(source, target []string) []string {
+	if j.permissionsMap == nil {
 		return target
 	}
 
 	for _, val := range source {
-		mappedValues, exist := j.scopeMap[val]
+		mappedValues, exist := j.permissionsMap[val]
 		if !exist {
 			// no mapping for value
 			continue
@@ -471,29 +471,29 @@ func (j *JWT) addMappedScopeValues(source, target []string) []string {
 		for _, mv := range mappedValues {
 			var added bool
 			// add value from mapping?
-			target, added = addScopeValue(target, mv)
+			target, added = addPermission(target, mv)
 			if !added {
 				continue
 			}
 			l = append(l, mv)
 		}
 		// recursion: call only with values not already in target
-		target = j.addMappedScopeValues(l, target)
+		target = j.addMappedPermissions(l, target)
 	}
 	return target
 }
 
-func addScopeValue(scopeValues []string, scope string) ([]string, bool) {
-	scope = strings.TrimSpace(scope)
-	if scope == "" {
-		return scopeValues, false
+func addPermission(permissions []string, permission string) ([]string, bool) {
+	permission = strings.TrimSpace(permission)
+	if permission == "" {
+		return permissions, false
 	}
-	for _, s := range scopeValues {
-		if s == scope {
-			return scopeValues, false
+	for _, p := range permissions {
+		if p == permission {
+			return permissions, false
 		}
 	}
-	return append(scopeValues, scope), true
+	return append(permissions, permission), true
 }
 
 func getBearer(val string) (string, error) {
diff --git a/accesscontrol/permissions.go b/accesscontrol/permissions.go
new file mode 100644
index 00000000..f9c828a6
--- /dev/null
+++ b/accesscontrol/permissions.go
@@ -0,0 +1,103 @@
+package accesscontrol
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/avenga/couper/config/request"
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/eval"
+	"github.com/avenga/couper/handler/middleware"
+)
+
+type requiredPermissions struct {
+	permission  string
+	permissions map[string]string // permission per method
+}
+
+func newRequiredPermissions(permission string, permissionMap map[string]string) requiredPermissions {
+	rp := requiredPermissions{}
+	if permissionMap == nil {
+		rp.permission = permission
+		return rp
+	}
+
+	rp.setPermissionMap(permissionMap)
+	return rp
+}
+
+func (r *requiredPermissions) setPermissionMap(permissionMap map[string]string) {
+	r.permissions = make(map[string]string)
+	if otherPermission, otherMethodExists := permissionMap["*"]; otherMethodExists {
+		for _, method := range middleware.DefaultEndpointAllowedMethods {
+			r.permissions[method] = otherPermission
+		}
+	}
+	for method, permission := range permissionMap {
+		if method == "*" {
+			continue
+		}
+		r.permissions[method] = permission
+	}
+}
+
+func (r *requiredPermissions) getPermission(method string) (string, error) {
+	if r.permissions == nil {
+		return r.permission, nil
+	}
+
+	permission, exists := r.permissions[method]
+	if !exists {
+		return "", errors.MethodNotAllowed.Messagef("method %s not allowed by beta_required_permission", method)
+	}
+	return permission, nil
+}
+
+var _ AccessControl = &PermissionsControl{}
+
+type PermissionsControl struct {
+	required requiredPermissions
+}
+
+func NewPermissionsControl(permission string, permissionMap map[string]string) *PermissionsControl {
+	rp := newRequiredPermissions(permission, permissionMap)
+	return &PermissionsControl{required: rp}
+}
+
+// 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 err != nil {
+		return err
+	}
+
+	if requiredPermission == "" {
+		return nil
+	}
+
+	ctx := req.Context()
+	ctx = context.WithValue(ctx, request.BetaRequiredPermission, requiredPermission)
+	*req = *req.WithContext(ctx)
+
+	evalCtx := eval.ContextFromRequest(req)
+	*req = *req.WithContext(evalCtx.WithClientRequest(req))
+
+	grantedPermission, ok := ctx.Value(request.BetaGrantedPermissions).([]string)
+	if !ok {
+		return errors.BetaInsufficientPermissions.Messagef("no permissions granted")
+	}
+	if !hasGrantedPermission(grantedPermission, requiredPermission) {
+		return errors.BetaInsufficientPermissions.Messagef("required permission %q not granted", requiredPermission)
+	}
+	return nil
+}
+
+// hasGrantedPermission checks whether a given permission is in the granted permissions
+func hasGrantedPermission(grantedPermissions []string, permission string) bool {
+	for _, gp := range grantedPermissions {
+		if gp == permission {
+			return true
+		}
+	}
+	return false
+}
diff --git a/accesscontrol/scope.go b/accesscontrol/scope.go
deleted file mode 100644
index 491874da..00000000
--- a/accesscontrol/scope.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package accesscontrol
-
-import (
-	"net/http"
-
-	"github.com/avenga/couper/config/request"
-	"github.com/avenga/couper/errors"
-)
-
-var supportedOperations = []string{
-	http.MethodGet,
-	http.MethodHead,
-	http.MethodPost,
-	http.MethodPut,
-	http.MethodPatch,
-	http.MethodDelete,
-	http.MethodConnect,
-	http.MethodOptions,
-	http.MethodTrace,
-}
-
-type requiredScope struct {
-	scopes map[string][]string
-}
-
-func newRequiredScope() requiredScope {
-	return requiredScope{scopes: make(map[string][]string)}
-}
-
-func (r *requiredScope) addScopeMap(scopeMap map[string]string) {
-	otherScope, otherMethodExists := scopeMap["*"]
-	for _, op := range supportedOperations {
-		scope, exists := scopeMap[op]
-		if exists {
-			r.addScopeForOperation(op, scope)
-		} else if otherMethodExists {
-			r.addScopeForOperation(op, otherScope)
-		} else {
-			delete(r.scopes, op)
-		}
-	}
-}
-
-func (r *requiredScope) addScopeForOperation(operation, scope string) {
-	scopes, exists := r.scopes[operation]
-	if !exists {
-		if scope == "" {
-			// method permitted without scope
-			r.scopes[operation] = []string{}
-			return
-		}
-		// method permitted with required scope
-		r.scopes[operation] = []string{scope}
-		return
-	}
-	// no additional scope required
-	if scope == "" {
-		return
-	}
-	// add scope to required scopes for this operation
-	r.scopes[operation] = append(scopes, scope)
-}
-
-var _ AccessControl = &ScopeControl{}
-
-type ScopeControl struct {
-	required requiredScope
-}
-
-func NewScopeControl(scopeMaps []map[string]string) *ScopeControl {
-	rs := newRequiredScope()
-	for _, scopeMap := range scopeMaps {
-		if scopeMap != nil {
-			rs.addScopeMap(scopeMap)
-		}
-	}
-	return &ScopeControl{required: rs}
-}
-
-// Validate validates the scope values provided by access controls against the required scope values.
-func (s *ScopeControl) Validate(req *http.Request) error {
-	if len(s.required.scopes) == 0 {
-		return nil
-	}
-	requiredScopes, exists := s.required.scopes[req.Method]
-	if !exists {
-		return errors.BetaOperationDenied.Messagef("operation %s not permitted", req.Method)
-	}
-	ctx := req.Context()
-	grantedScope, ok := ctx.Value(request.Scopes).([]string)
-	if !ok && len(requiredScopes) > 0 {
-		return errors.BetaInsufficientScope.Messagef("no scope granted")
-	}
-	for _, rs := range requiredScopes {
-		if !hasGrantedScope(grantedScope, rs) {
-			return errors.BetaInsufficientScope.Messagef("required scope %q not granted", rs)
-		}
-	}
-	return nil
-}
-
-func hasGrantedScope(grantedScope []string, scope string) bool {
-	for _, gs := range grantedScope {
-		if gs == scope {
-			return true
-		}
-	}
-	return false
-}
diff --git a/config/ac_jwt.go b/config/ac_jwt.go
index 2316daf5..16ae7fca 100644
--- a/config/ac_jwt.go
+++ b/config/ac_jwt.go
@@ -31,8 +31,8 @@ type JWT struct {
 	Remain                hcl.Body            `hcl:",remain"`
 	RolesClaim            string              `hcl:"beta_roles_claim,optional"`
 	RolesMap              map[string][]string `hcl:"beta_roles_map,optional"`
-	ScopeClaim            string              `hcl:"beta_scope_claim,optional"`
-	ScopeMap              map[string][]string `hcl:"beta_scope_map,optional"`
+	PermissionsClaim      string              `hcl:"beta_permissions_claim,optional"`
+	PermissionsMap        map[string][]string `hcl:"beta_permissions_map,optional"`
 	SignatureAlgorithm    string              `hcl:"signature_algorithm,optional"`
 	SigningKey            string              `hcl:"signing_key,optional"`
 	SigningKeyFile        string              `hcl:"signing_key_file,optional"`
diff --git a/config/api.go b/config/api.go
index fe0689a6..ac07d505 100644
--- a/config/api.go
+++ b/config/api.go
@@ -20,7 +20,7 @@ type API struct {
 	ErrorFile            string    `hcl:"error_file,optional"`
 	Name                 string    `hcl:"name,label"`
 	Remain               hcl.Body  `hcl:",remain"`
-	Scope                cty.Value `hcl:"beta_scope,optional"`
+	RequiredPermission   cty.Value `hcl:"beta_required_permission,optional"`
 
 	// internally used
 	CatchAllEndpoint *Endpoint
diff --git a/config/configload/schema.go b/config/configload/schema.go
index 2dc87480..993a32d8 100644
--- a/config/configload/schema.go
+++ b/config/configload/schema.go
@@ -266,7 +266,7 @@ func completeSchemaComponents(body hcl.Body, schema *hcl.BodySchema, attrs hcl.A
 
 						keyName := seetie.ValueToString(k)
 						switch name {
-						case "add_request_headers", "add_response_headers", "beta_scope", "headers", "set_request_headers", "set_response_headers":
+						case "add_request_headers", "add_response_headers", "beta_required_permission", "headers", "set_request_headers", "set_response_headers":
 							// header field names, method names: handle object keys case-insensitively
 							keyName = strings.ToLower(keyName)
 						}
diff --git a/config/endpoint.go b/config/endpoint.go
index 6436751c..5ca20054 100644
--- a/config/endpoint.go
+++ b/config/endpoint.go
@@ -21,7 +21,7 @@ type Endpoint struct {
 	Remain               hcl.Body  `hcl:",remain"`
 	RequestBodyLimit     string    `hcl:"request_body_limit,optional"`
 	Response             *Response `hcl:"response,block"`
-	Scope                cty.Value `hcl:"beta_scope,optional"`
+	RequiredPermission   cty.Value `hcl:"beta_required_permission,optional"`
 
 	// internally configured due to multi-label options
 	Proxies   Proxies
diff --git a/config/request/context_key.go b/config/request/context_key.go
index ff49191d..f8b1af69 100644
--- a/config/request/context_key.go
+++ b/config/request/context_key.go
@@ -8,6 +8,8 @@ const (
 	AccessControls
 	BackendName
 	BackendParams
+	BetaGrantedPermissions
+	BetaRequiredPermission
 	BufferOptions
 	ConnectTimeout
 	ContextVariablesSynced
@@ -27,7 +29,6 @@ const (
 	ResponseWriter
 	RoundTripName
 	RoundTripProxy
-	Scopes
 	ServerName
 	StartTime
 	TokenRequest
diff --git a/config/runtime/access_control.go b/config/runtime/access_control.go
index a316e46f..78c3d885 100644
--- a/config/runtime/access_control.go
+++ b/config/runtime/access_control.go
@@ -7,6 +7,7 @@ import (
 	"github.com/avenga/couper/accesscontrol"
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/eval"
 )
 
 type ACDefinitions map[string]*AccessControl
@@ -21,7 +22,7 @@ func (m ACDefinitions) Add(name string, ac accesscontrol.AccessControl, eh []*co
 	if n == "" {
 		return errors.Configuration.Message("accessControl requires a label")
 	}
-	if n == "scopes" {
+	if eval.IsReservedContextName(n) {
 		return errors.Configuration.Message("accessControl uses reserved name as label")
 	}
 	if _, ok := m[n]; ok {
diff --git a/config/runtime/error_handler.go b/config/runtime/error_handler.go
index 2d8d474f..e77e4ed7 100644
--- a/config/runtime/error_handler.go
+++ b/config/runtime/error_handler.go
@@ -13,8 +13,8 @@ import (
 )
 
 var wildcardMap = map[string][]string{
-	"api":      {"beta_insufficient_scope", "beta_operation_denied"},
-	"endpoint": {"beta_insufficient_scope", "beta_operation_denied", "sequence", "unexpected_status"},
+	"api":      {"beta_insufficient_permissions"},
+	"endpoint": {"beta_insufficient_permissions", "sequence", "unexpected_status"},
 }
 
 func newErrorHandler(ctx *hcl.EvalContext, opts *protectedOptions, log *logrus.Entry,
diff --git a/config/runtime/server.go b/config/runtime/server.go
index 89cca866..38313bd1 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -14,6 +14,7 @@ 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"
@@ -77,21 +78,6 @@ func GetHostPort(hostPort string) (string, int, error) {
 	return host, port, nil
 }
 
-var defaultFileSpaAllowedMethods = []string{
-	http.MethodGet,
-	http.MethodHead,
-}
-
-var defaultEndpointAllowedMethods = []string{
-	http.MethodGet,
-	http.MethodHead,
-	http.MethodPost,
-	http.MethodPut,
-	http.MethodPatch,
-	http.MethodDelete,
-	http.MethodOptions,
-}
-
 // NewServerConfiguration sets http handler specific defaults and validates the given gateway configuration.
 // Wire up all endpoints and maps them within the returned Server.
 func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *cache.MemoryStore) (ServerConfiguration, error) {
@@ -168,7 +154,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 
 			epOpts := &handler.EndpointOptions{ErrorTemplate: serverOptions.ServerErrTpl}
 			notAllowedMethodsHandler := epOpts.ErrorTemplate.WithError(errors.MethodNotAllowed)
-			allowedMethodsHandler := middleware.NewAllowedMethodsHandler(nil, defaultFileSpaAllowedMethods, spaHandler, notAllowedMethodsHandler)
+			allowedMethodsHandler := middleware.NewAllowedMethodsHandler(nil, middleware.DefaultFileSpaAllowedMethods, spaHandler, notAllowedMethodsHandler)
 			spaHandler = allowedMethodsHandler
 
 			spaHandler, err = configureProtectedHandler(accessControls, confCtx,
@@ -214,7 +200,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 
 			epOpts := &handler.EndpointOptions{ErrorTemplate: serverOptions.FilesErrTpl}
 			notAllowedMethodsHandler := epOpts.ErrorTemplate.WithError(errors.MethodNotAllowed)
-			allowedMethodsHandler := middleware.NewAllowedMethodsHandler(nil, defaultFileSpaAllowedMethods, fileHandler, notAllowedMethodsHandler)
+			allowedMethodsHandler := middleware.NewAllowedMethodsHandler(nil, middleware.DefaultFileSpaAllowedMethods, fileHandler, notAllowedMethodsHandler)
 			fileHandler = allowedMethodsHandler
 
 			fileHandler, err = configureProtectedHandler(accessControls, confCtx,
@@ -319,23 +305,31 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				}
 				epHandler = handler.NewEndpoint(epOpts, log, modifier)
 
-				scopeMaps, err := newScopeMaps(parentAPI, endpointConf)
-				if err != nil {
-					return nil, err
+				requiredPermissionVal := endpointConf.RequiredPermission
+				if requiredPermissionVal == cty.NilVal && parentAPI != nil {
+					// if required permission in endpoint {} not defined, try required permission in parent api {}
+					requiredPermissionVal = parentAPI.RequiredPermission
 				}
-
-				scopeControl := ac.NewScopeControl(scopeMaps)
-				scopeErrorHandler, err := newErrorHandler(confCtx, &protectedOptions{
-					epOpts:   epOpts,
-					memStore: memStore,
-					settings: conf.Settings,
-					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 requiredPermissionVal == cty.NilVal {
+					protectedHandler = epHandler
+				} else {
+					requiredPermission, requiredPermissionMap, err := seetie.ValueToPermission(requiredPermissionVal)
+					if err != nil {
+						return nil, err
+					}
+					permissionsControl := ac.NewPermissionsControl(requiredPermission, requiredPermissionMap)
+					permissionsErrorHandler, err := newErrorHandler(confCtx, &protectedOptions{
+						epOpts:   epOpts,
+						memStore: memStore,
+						settings: conf.Settings,
+						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
+					}
+
+					protectedHandler = middleware.NewErrorHandler(permissionsControl.Validate, permissionsErrorHandler)(epHandler)
 				}
-
-				protectedHandler = middleware.NewErrorHandler(scopeControl.Validate, scopeErrorHandler)(epHandler)
 			}
 
 			accessControl := newAC(srvConf, parentAPI)
@@ -346,7 +340,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
 				allowedMethods = parentAPI.AllowedMethods
 			}
 			notAllowedMethodsHandler := epOpts.ErrorTemplate.WithError(errors.MethodNotAllowed)
-			allowedMethodsHandler := middleware.NewAllowedMethodsHandler(allowedMethods, defaultEndpointAllowedMethods, protectedHandler, notAllowedMethodsHandler)
+			allowedMethodsHandler := middleware.NewAllowedMethodsHandler(allowedMethods, middleware.DefaultEndpointAllowedMethods, protectedHandler, notAllowedMethodsHandler)
 			protectedHandler = allowedMethodsHandler
 
 			epHandler, err = configureProtectedHandler(accessControls, confCtx, accessControl,
@@ -423,29 +417,6 @@ func bodiesWithACBodies(defs *config.Definitions, ac, dac []string) []hcl.Body {
 	return bodies
 }
 
-func newScopeMaps(parentAPI *config.API, endpoint *config.Endpoint) ([]map[string]string, error) {
-	var scopeMaps []map[string]string
-	if parentAPI != nil {
-		apiScopeMap, err := seetie.ValueToScopeMap(parentAPI.Scope)
-		if err != nil {
-			return nil, err
-		}
-		if apiScopeMap != nil {
-			scopeMaps = append(scopeMaps, apiScopeMap)
-		}
-	}
-	endpointScopeMap, err := seetie.ValueToScopeMap(endpoint.Scope)
-	if err != nil {
-		return nil, err
-	}
-
-	if endpointScopeMap != nil {
-		scopeMaps = append(scopeMaps, endpointScopeMap)
-	}
-
-	return scopeMaps, nil
-}
-
 func whichCORS(parent *config.Server, this interface{}) *config.CORS {
 	val := reflect.ValueOf(this)
 	if val.IsZero() {
@@ -592,8 +563,8 @@ func newJWT(jwtConf *config.JWT, conf *config.Couper, confCtx *hcl.EvalContext,
 		Name:                  jwtConf.Name,
 		RolesClaim:            jwtConf.RolesClaim,
 		RolesMap:              jwtConf.RolesMap,
-		ScopeClaim:            jwtConf.ScopeClaim,
-		ScopeMap:              jwtConf.ScopeMap,
+		PermissionsClaim:      jwtConf.PermissionsClaim,
+		PermissionsMap:        jwtConf.PermissionsMap,
 		Source:                ac.NewJWTSource(jwtConf.Cookie, jwtConf.Header, jwtConf.TokenValue),
 	}
 	var (
diff --git a/docs/ERRORS.md b/docs/ERRORS.md
index 4cb6059a..bd4eaa4d 100644
--- a/docs/ERRORS.md
+++ b/docs/ERRORS.md
@@ -4,7 +4,7 @@
   - [Introduction](#introduction)
   - [Error messages](#error-messages)
   - [Access control error_handler](#access-control-error_handler)
-  - [Scope related error_handler](#scope-related-error_handler)
+  - [Permissions related error_handler](#permissions-related-error_handler)
   - [Endpoint related error_handler](#endpoint-related-error_handler)
   - [Error types](#error-types)
     - [Access control error types](#access-control-error-types)
@@ -24,13 +24,11 @@ Detailed information is provided via log message. This way, all information can
 ## Access control `error_handler`
 
 Access control errors in particular require special handling, e.g. sending a specific response for missing login credentials.
-For this purpose every access control definition of `basic_auth`, `jwt` or `saml2` can define one or multiple [`error_handler` blocks](REFERENCE.md#error-handler-block) with one or more defined error type labels listed below.
+For this purpose every access control definition of `basic_auth`, `jwt`, `oidc` or `saml2` can define one or multiple [`error_handler` blocks](REFERENCE.md#error-handler-block) with one or more defined error type labels listed below.
 
-## Scope related `error_handler`
+## Permissions related `error_handler`
 
-Since an [AC](#access-control-error_handler) error handler is rather general than scopes handled per endpoint or http-method but are still protected with the
-same access-control. Reacting to those more path specific errors you can define one or more [`error_handler` blocks](REFERENCE.md#error-handler-block) within
-an `api` or `endpoint` block.
+Required permissions are configured for `api` or `endpoint` blocks. So errors caused by insufficient permissions can be handled at API or endpoint levels.
 
 ## Endpoint related `error_handler`
 
@@ -56,11 +54,9 @@ All errors have a specific type. You can find it in the log field `error_type`.
 
 ### API and endpoint error types
 
-| Type (and super types)                          | Description                                                                                      | Default handling                                                            |
-|:------------------------------------------------|:-------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------|
-| `beta_scope`                                    | All `beta_scope` related errors                                                                  | Send error template with status `403`.                                      |
-| `beta_insufficient_scope` (`beta_scope`)        | The request is not in the scope granted to the requester.                                        | Send error template with status `403`.                                      |
-| `beta_operation_denied`                         | The request method is not permitted.                                                             | Send error template with status `403`.                                      |
+| Type (and super types)                          | Description                                                                                             | Default handling                                                            |
+|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------|
+| `beta_insufficient_permissions`                 | The permission required for the requested operation is not in the permissions granted to the requester. | Send error template with status `403`.                                      |
 
 ### Endpoint error types
 
diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index 3cb5d793..5e888f3b 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -117,7 +117,7 @@ as json error with an error body payload. This can be customized via `error_file
 | `error_file` |string|-|Location of the error file template.|-|`error_file = "./my_error_body.json"`|
 | `access_control` |list|-|Sets predefined [Access Control](#access-control) for `api` block context.|&#9888; Inherited by nested blocks.| `access_control = ["foo"]`|
 | `allowed_methods` | tuple of 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_scope` |string or object|-|Scope value required to use this API (see [error type](ERRORS.md#error-types) `beta_insufficient_scope`).|If the value is a string, the same scope value applies to all request methods. If there are different scope values 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 (see [error type](ERRORS.md#error-types) `beta_operation_denied`). `"*"` is the key for "all other methods". A value `""` means "no (additional) scope required".| `beta_scope = "read"` or `beta_scope = { post = "write", "*" = "" }`|
+| `beta_required_permission` |string or object|-|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", "*" = "" }`|
 | `custom_log_fields` | map | - | Defines log fields for [Custom Logging](LOGS.md#custom-logging). | &#9888; Inherited by nested blocks. | - |
 
 ### Endpoint Block
@@ -140,7 +140,7 @@ produce an explicit or implicit client response.
 | `path`                  | string           | -       | Changeable part of the upstream URL. Changes the path suffix of the outgoing request.                             | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | -                                                                    |
 | `access_control`        | list             | -       | Sets predefined [Access Control](#access-control) for `endpoint` block context.                                   | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | `access_control = ["foo"]`                                           |
 | `allowed_methods`       | tuple of 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_scope`            | string or object | -       | Scope value required to use this endpoint (see [error type](ERRORS.md#error-types) `beta_insufficient_scope`). | If the value is a string, the same scope value applies to all request methods. If there are different scope values 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 (see [error type](ERRORS.md#error-types) `beta_operation_denied`). `"*"` is the key for "all other methods". A value `""` means "no (additional) scope required". | `beta_scope = "read"` or `beta_scope = { post = "write", "*" = "" }` |
+| `beta_required_permission` | string or object | -    | 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". | `beta_required_permission = "read"` or `beta_required_permission = { post = "write", "*" = "" }` |
 | `custom_log_fields`     | map              | -       | Defines log fields for [Custom Logging](LOGS.md#custom-logging).                                                  | &#9888; Inherited by nested blocks.                                                                                                                                                                                                                                                                                                                                                                                                             | -                                                                    |
 | [Modifiers](#modifiers) | -                | -       | -                                                                                                                 | -                                                                                                                                                                                                                                                                                                                                                                                                                                               | -                                                                    |
 
@@ -306,7 +306,7 @@ The `oauth2` block in the [Backend Block](#backend-block) context configures the
 | `client_secret` |string|-|The client password.|&#9888; required.|-|
 | `retries` |integer|`1` | The number of retries to get the token and resource, if the resource-request responds with `401 Unauthorized` HTTP status code.|-|-|
 | `token_endpoint_auth_method` |string|`client_secret_basic`|Defines the method to authenticate the client at the token endpoint.|If set to `client_secret_post`, the client credentials are transported in the request body. If set to `client_secret_basic`, the client credentials are transported via Basic Authentication.|-|
-| `scope`                      |string|-|  A space separated list of requested scopes for the access token.|-| `scope = "read write"` |
+| `scope`                      |string|-|  A space separated list of requested scope values for the access token.|-| `scope = "read write"` |
 
 The HTTP header field `Accept: application/json` is automatically added to the token request. This can be modified with [request header modifiers](#request-header) in a [backend block](#backend-block).
 
@@ -385,10 +385,10 @@ Since responses from endpoints protected by JWT access controls are not publicly
 | `signature_algorithm`           |string|-|-|Valid values: `RS256` `RS384` `RS512` `HS256` `HS384` `HS512` `ES256` `ES384` `ES512`|-|
 | `claims`               |object|-|Object with claims that must be given for a valid token (equals comparison with JWT payload).| The claim values are evaluated per request. | `claims = { pid = request.path_params.pid }` |
 | `required_claims`      |string|-|List of claim names that must be given for a valid token |-|`required_claims = ["roles"]`|
-| `beta_scope_claim` |string|-|name of claim specifying the scope of token|The claim value must either be a string containing a space-separated list of scope values or a list of string scope values|`beta_scope_claim = "scope"`|
-| `beta_scope_map` |map|-| mapping of scope values to additional scope values | Maps values from `beta_scope_claim` and those created from `beta_roles_map`. The map is called recursively. |`beta_scope_map = { sc1 = ["sc3", "sc4"], sc2 = ["sc5"] }`|
+| `beta_permissions_claim` |string|-|name of claim containing the granted permissions|The claim value must either be a string containing a space-separated list of permissions or a list of string permissions|`beta_permissions_claim = "scope"`|
+| `beta_permissions_map` |map|-| mapping of granted permissions to additional granted permissions | Maps values from `beta_permissions_claim` and those created from `beta_roles_map`. The map is called recursively. |`beta_permissions_map = { p1 = ["p3", "p4"], p2 = ["p5"] }`|
 | `beta_roles_claim` |string|-|name of claim specifying the roles of the user represented by the token|The claim value must either be a string containing a space-separated list of role values or a list of string role values|`beta_roles_claim = "roles"`|
-| `beta_roles_map` |map|-| mapping of roles to scope values | Non-mapped roles can be assigned with `*` to specific claims. |`beta_roles_map = { role1 = ["scope1", "scope2"], role2 = ["scope3"], "*" = ["public"] }`|
+| `beta_roles_map` |map|-| mapping of roles to granted permissions | Non-mapped roles can be assigned with `*` to specific permissions. |`beta_roles_map = { role1 = ["p1", "p2"], role2 = ["p3"], "*" = ["public"] }`|
 | `jwks_url` | string | - | URI pointing to a set of [JSON Web Keys (RFC 7517)](https://datatracker.ietf.org/doc/html/rfc7517) | - | `jwks_url = "http://identityprovider:8080/jwks.json"` |
 | `jwks_ttl` | [duration](#duration) | `"1h"` | Time period the JWK set stays valid and may be cached. | - | `jwks_ttl = "1800s"` |
 | `backend`  | string| - | [backend reference](#backend-block) for enhancing JWKS requests| - | `backend = "jwks_backend"` |
@@ -400,9 +400,9 @@ If all three attributes are missing, `header = "Authorization"` will be implied,
 If the key to verify the signatures of tokens does not change over time, it should be specified via either `key` or `key_file` (together with `signature_algorithm`).
 Otherwise, a JSON web key set should be referenced via `jwks_url`; in this case, the tokens need a `kid` header.
 
-A JWT access control configured by this block can extract scope values from
+A JWT access control configured by this block can extract permissions from
 
-- the value of the claim specified by `beta_scope_claim` and
+- the value of the claim specified by `beta_permissions_claim` and
 - the result of mapping the value of the claim specified by `beta_roles_claim` using the `beta_roles_map`.
 
 The `jwt` block may also be referenced by the [`jwt_sign()` function](#functions), if it has a `signing_ttl` defined. For `HS*` algorithms the signing key is taken from `key`/`key_file`, for `RS*` and `ES*` algorithms, `signing_key` or `signing_key_file` have to be specified.
@@ -458,7 +458,7 @@ Like all [Access Control](#access-control) types, the `beta_oauth2` block is def
 | `grant_type` |string|-| The grant type. |&#9888; required, to be set to: `authorization_code`|`grant_type = "authorization_code"`|
 | `client_id`|  string|-|The client identifier.|&#9888; required|-|
 | `client_secret` |string|-|The client password.|&#9888; required.|-|
-| `scope` |string|-| A space separated list of requested scopes for the access token.| - | `scope = "read write"` |
+| `scope` |string|-| A space separated list of requested scope values for the access token.| - | `scope = "read write"` |
 | `verifier_method` | string | - | The method to verify the integrity of the authorization code flow | &#9888; required, available values: `ccm_s256` (`code_challenge` parameter with `code_challenge_method` `S256`), `state` (`state` parameter) | `verifier_method = "ccm_s256"` |
 | `verifier_value` | string or expression | - | The value of the (unhashed) verifier. | &#9888; required; e.g. using cookie value created with [`oauth2_verifier()` function](#functions) | `verifier_value = request.cookies.verifier` |
 | `custom_log_fields` | map | - | Defines log fields for [Custom Logging](LOGS.md#custom-logging). | &#9888; Inherited by nested blocks. | - |
@@ -487,7 +487,7 @@ Like all [Access Control](#access-control) types, the `oidc` block is defined in
 | `redirect_uri`               | string                | -                     | The Couper endpoint for receiving the authorization code.                      | &#9888; required. Relative URL references are resolved against the origin of the current request URL. The origin can be changed with the [`accept_forwarded_url`](#settings-block) attribute if Couper is running behind a proxy. | -                                           |
 | `client_id`                  | string                | -                     | The client identifier.                                                         | &#9888; required                                                                                                                                                                                                                  | -                                           |
 | `client_secret`              | string                | -                     | The client password.                                                           | &#9888; required.                                                                                                                                                                                                                 | -                                           |
-| `scope`                      | string                | -                     | A space separated list of requested scopes for the access token.               | `openid` is automatically added.                                                                                                                                                                                                  | `scope = "profile read"`                    |
+| `scope`                      | string                | -                     | A space separated list of requested scope values for the access token.               | `openid` is automatically added.                                                                                                                                                                                            | `scope = "profile read"`                    |
 | `verifier_method`            | string                | -                     | The method to verify the integrity of the authorization code flow              | available values: `ccm_s256` (`code_challenge` parameter with `code_challenge_method` `S256`), `nonce` (`nonce` parameter)                                                                                                        | `verifier_method = "nonce"`                 |
 | `verifier_value`             | string or expression  | -                     | The value of the (unhashed) verifier.                                          | &#9888; required; e.g. using cookie value created with [`oauth2_verifier()` function](#functions)                                                                                                                                 | `verifier_value = request.cookies.verifier` |
 | `custom_log_fields`          | map                   | -                     | Defines log fields for [Custom Logging](LOGS.md#custom-logging).               | &#9888; Inherited by nested blocks.                                                                                                                                                                                               | -                                           |
@@ -619,7 +619,7 @@ Examples:
 ## Access Control
 
 The configuration of access control is twofold in Couper: You define the particular
-type (such as `jwt` or `basic_auth`) in `definitions`, each with a distinct label (must not be one of the reserved names: `scopes`).
+type (such as `jwt` or `basic_auth`) in `definitions`, each with a distinct label (must not be one of the reserved names: `beta_granted_permissions`, `beta_required_permission`).
 Anywhere in the `server` block those labels can be used in the `access_control`
 list to protect that block. &#9888; access rights are inherited by nested blocks.
 You can also disable `access_control` for blocks. By typing `disable_access_control = ["bar"]`,
@@ -678,6 +678,8 @@ defaults {
 | `body`                           | string          | Request message body                                                                                                                                                                                                                                                                |                                             |
 | `form_body.<name>`               | tuple of string | Parameter in a `application/x-www-form-urlencoded` body                                                                                                                                                                                                                             |                                             |
 | `json_body.<name>`               | various         | Access json decoded object properties. Media type must be `application/json` or `application/*+json`.                                                                                                                                                                               |                                             |
+| `context.beta_granted_permissions` | tuple of string | Permissions granted to the requester as yielded by access controls (see e.g. `beta_permissions_claim`, `beta_roles_claim` in the [`jwt` block](#jwt-block)).                                                                                                                      | `["perm1", "perm2"]`                        |
+| `context.beta_required_permission` | string        | Permission required to perform the requested operation (value of the `beta_required_permission` attribute of [`endpoint`](#endpoint-block) (or [`api`](#api-block)) block).                                                                                                         |                                             |
 | `context.<name>.<property_name>` | various         | Request context containing information from the [Access Control](#access-control).                                                                                                                                                                                                  |                                             |
 | `url`                            | string          | Request URL                                                                                                                                                                                                                                                                         | `https://www.example.com/path/to?q=val&a=1` |
 | `origin`                         | string          | Origin of the request URL                                                                                                                                                                                                                                                           | `https://www.example.com`                   |
diff --git a/errors/type_defintions.go b/errors/type_defintions.go
index 16c687bb..11467faa 100644
--- a/errors/type_defintions.go
+++ b/errors/type_defintions.go
@@ -20,10 +20,7 @@ var Definitions = []*Error{
 	AccessControl.Kind("saml2"),
 	AccessControl.Kind("saml2").Kind("saml"),
 
-	AccessControl.Kind("beta_operation_denied"),
-
-	AccessControl.Kind("beta_scope"),
-	AccessControl.Kind("beta_scope").Kind("beta_insufficient_scope"),
+	AccessControl.Kind("beta_insufficient_permissions"),
 
 	BackendValidation,
 
diff --git a/errors/types_generated.go b/errors/types_generated.go
index b3505a18..22d13b76 100644
--- a/errors/types_generated.go
+++ b/errors/types_generated.go
@@ -12,11 +12,9 @@ var (
 	Oauth2                      = Definitions[7]
 	Saml2                       = Definitions[8]
 	Saml                        = Definitions[9]
-	BetaOperationDenied         = Definitions[10]
-	BetaScope                   = Definitions[11]
-	BetaInsufficientScope       = Definitions[12]
-	Sequence                    = Definitions[14]
-	UnexpectedStatus            = Definitions[15]
+	BetaInsufficientPermissions = Definitions[10]
+	Sequence                    = Definitions[12]
+	UnexpectedStatus            = Definitions[13]
 )
 
 // typeDefinitions holds all related error definitions which are
@@ -36,9 +34,7 @@ var types = typeDefinitions{
 	"oauth2":                         Oauth2,
 	"saml2":                          Saml2,
 	"saml":                           Saml,
-	"beta_operation_denied":          BetaOperationDenied,
-	"beta_scope":                     BetaScope,
-	"beta_insufficient_scope":        BetaInsufficientScope,
+	"beta_insufficient_permissions":  BetaInsufficientPermissions,
 	"backend_validation":             BackendValidation,
 	"sequence":                       Sequence,
 	"unexpected_status":              UnexpectedStatus,
diff --git a/eval/context.go b/eval/context.go
index 205a1668..305ba994 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -494,9 +494,22 @@ func NewRawOrigin(u *url.URL) *url.URL {
 	return &rawOrigin
 }
 
+const (
+	betaGrantedPermissions = "beta_granted_permissions"
+	betaRequiredPermission = "beta_required_permission"
+)
+
+func IsReservedContextName(name string) bool {
+	switch name {
+	case betaGrantedPermissions, betaRequiredPermission:
+		return true
+	}
+
+	return false
+}
+
 func newVariable(ctx context.Context, cookies []*http.Cookie, headers http.Header) ContextMap {
 	acData, _ := ctx.Value(request.AccessControls).(map[string]interface{})
-	scopeData, _ := ctx.Value(request.Scopes).([]string)
 	ctxAcMap := make(map[string]cty.Value)
 	for name, data := range acData {
 		dataMap, ok := data.(map[string]interface{})
@@ -505,8 +518,12 @@ func newVariable(ctx context.Context, cookies []*http.Cookie, headers http.Heade
 		}
 		ctxAcMap[name] = seetie.MapToValue(dataMap)
 	}
-	if len(scopeData) > 0 {
-		ctxAcMap["scopes"] = seetie.GoToValue(scopeData)
+	grantedPermissions, _ := ctx.Value(request.BetaGrantedPermissions).([]string)
+	if len(grantedPermissions) > 0 {
+		ctxAcMap[betaGrantedPermissions] = seetie.GoToValue(grantedPermissions)
+	}
+	if requiredPermission, permissionSet := ctx.Value(request.BetaRequiredPermission).(string); permissionSet {
+		ctxAcMap[betaRequiredPermission] = seetie.GoToValue(requiredPermission)
 	}
 	var ctxAcMapValue cty.Value
 	if len(ctxAcMap) > 0 {
diff --git a/handler/middleware/allowed_methods.go b/handler/middleware/allowed_methods.go
index c2e9552c..793900ad 100644
--- a/handler/middleware/allowed_methods.go
+++ b/handler/middleware/allowed_methods.go
@@ -5,6 +5,21 @@ import (
 	"strings"
 )
 
+var DefaultFileSpaAllowedMethods = []string{
+	http.MethodGet,
+	http.MethodHead,
+}
+
+var DefaultEndpointAllowedMethods = []string{
+	http.MethodGet,
+	http.MethodHead,
+	http.MethodPost,
+	http.MethodPut,
+	http.MethodPatch,
+	http.MethodDelete,
+	http.MethodOptions,
+}
+
 var _ http.Handler = &AllowedMethodsHandler{}
 
 type AllowedMethodsHandler struct {
diff --git a/internal/seetie/convert.go b/internal/seetie/convert.go
index 304812b7..7531e948 100644
--- a/internal/seetie/convert.go
+++ b/internal/seetie/convert.go
@@ -55,26 +55,25 @@ func ValueToMap(val cty.Value) map[string]interface{} {
 	return result
 }
 
-func ValueToScopeMap(val cty.Value) (map[string]string, error) {
-	scopeMap := make(map[string]string)
+func ValueToPermission(val cty.Value) (string, map[string]string, error) {
 	switch val.Type() {
 	case cty.NilType:
-		return nil, nil
+		return "", nil, nil
 	case cty.String:
-		scopeMap["*"] = val.AsString()
-		return scopeMap, nil
+		return val.AsString(), nil, nil
 	default:
 		if val.Type().IsObjectType() {
+			permissionMap := make(map[string]string)
 			for k, v := range val.AsValueMap() {
 				if v.Type() != cty.String {
-					return nil, fmt.Errorf("unsupported value for operation %q in beta_scope", k)
+					return "", nil, fmt.Errorf("unsupported value for method %q in beta_required_permission", k)
 				}
-				scopeMap[strings.ToUpper(k)] = v.AsString()
+				permissionMap[strings.ToUpper(k)] = v.AsString()
 			}
-			return scopeMap, nil
+			return "", permissionMap, nil
 		}
 	}
-	return nil, fmt.Errorf("unsupported value for beta_scope")
+	return "", nil, fmt.Errorf("unsupported value for beta_required_permission")
 }
 
 func ValuesMapToValue(m url.Values) cty.Value {


Test patch:
diff --git a/accesscontrol/jwt_test.go b/accesscontrol/jwt_test.go
index 672c0608..08374105 100644
--- a/accesscontrol/jwt_test.go
+++ b/accesscontrol/jwt_test.go
@@ -333,7 +333,7 @@ func Test_JWT_Validate(t *testing.T) {
 	}
 }
 
-func Test_JWT_yields_scopes(t *testing.T) {
+func Test_JWT_yields_permissions(t *testing.T) {
 	log, hook := test.NewLogger()
 	signingMethod := jwt.SigningMethodHS256
 	algo := acjwt.NewAlgorithm(signingMethod.Alg())
@@ -344,28 +344,28 @@ func Test_JWT_yields_scopes(t *testing.T) {
 		"user2": {"bar"},
 		"*":     {"default"},
 	}
-	var noScope []string
+	var noGrantedPermissions []string
 
 	tests := []struct {
-		name       string
-		scopeClaim string
-		scope      interface{}
-		rolesClaim string
-		roles      interface{}
-		expWarning string
-		expScopes  []string
+		name             string
+		permissionsClaim string
+		permissionsValue interface{}
+		rolesClaim       string
+		rolesValue       interface{}
+		expWarning       string
+		expGrantedPerms  []string
 	}{
 		{
-			"no scope, no roles",
+			"no permissions, no roles",
 			"scp",
 			nil,
 			"roles",
 			nil,
 			"",
-			noScope,
+			noGrantedPermissions,
 		},
 		{
-			"scope: space-separated list",
+			"permissions: space-separated list",
 			"scp",
 			"foo bar",
 			"",
@@ -374,7 +374,7 @@ func Test_JWT_yields_scopes(t *testing.T) {
 			[]string{"foo", "bar"},
 		},
 		{
-			"scope: space-separated list, multiple",
+			"permissions: space-separated list, multiple",
 			"scp",
 			"foo bar foo",
 			"",
@@ -383,7 +383,7 @@ func Test_JWT_yields_scopes(t *testing.T) {
 			[]string{"foo", "bar"},
 		},
 		{
-			"scope: list of string",
+			"permissions: list of string",
 			"scoop",
 			[]string{"foo", "bar"},
 			"",
@@ -392,7 +392,7 @@ func Test_JWT_yields_scopes(t *testing.T) {
 			[]string{"foo", "bar"},
 		},
 		{
-			"scope: list of string, multiple",
+			"permissions: list of string, multiple",
 			"scoop",
 			[]string{"foo", "bar", "bar"},
 			"",
@@ -401,58 +401,58 @@ func Test_JWT_yields_scopes(t *testing.T) {
 			[]string{"foo", "bar"},
 		},
 		{
-			"scope: warn: boolean",
+			"permissions: warn: boolean",
 			"scope",
 			true,
 			"",
 			nil,
-			"invalid scope claim value type, ignoring claim, value true",
-			noScope,
+			"invalid permissions claim value type, ignoring claim, value true",
+			noGrantedPermissions,
 		},
 		{
-			"scope: warn: number",
+			"permissions: warn: number",
 			"scope",
 			1.23,
 			"",
 			nil,
-			"invalid scope claim value type, ignoring claim, value 1.23",
-			noScope,
+			"invalid permissions claim value type, ignoring claim, value 1.23",
+			noGrantedPermissions,
 		},
 		{
-			"scope: warn: list of bool",
+			"permissions: warn: list of bool",
 			"scope",
 			[]bool{true, false},
 			"",
 			nil,
-			"invalid scope claim value type, ignoring claim, value []interface {}{true, false}",
-			noScope,
+			"invalid permissions claim value type, ignoring claim, value []interface {}{true, false}",
+			noGrantedPermissions,
 		},
 		{
-			"scope: warn: list of number",
+			"permissions: warn: list of number",
 			"scope",
 			[]int{1, 2},
 			"",
 			nil,
-			"invalid scope claim value type, ignoring claim, value []interface {}{1, 2}",
-			noScope,
+			"invalid permissions claim value type, ignoring claim, value []interface {}{1, 2}",
+			noGrantedPermissions,
 		},
 		{
-			"scope: warn: mixed list",
+			"permissions: warn: mixed list",
 			"scope",
 			[]interface{}{"eins", 2},
 			"",
 			nil,
-			`invalid scope claim value type, ignoring claim, value []interface {}{"eins", 2}`,
-			noScope,
+			`invalid permissions claim value type, ignoring claim, value []interface {}{"eins", 2}`,
+			noGrantedPermissions,
 		},
 		{
-			"scope: warn: object",
+			"permissions: warn: object",
 			"scope",
 			map[string]interface{}{"foo": 1, "bar": 1},
 			"",
 			nil,
-			`invalid scope claim value type, ignoring claim, value map[string]interface {}{"bar":1, "foo":1}`,
-			noScope,
+			`invalid permissions claim value type, ignoring claim, value map[string]interface {}{"bar":1, "foo":1}`,
+			noGrantedPermissions,
 		},
 		{
 			"roles: single string",
@@ -594,11 +594,11 @@ func Test_JWT_yields_scopes(t *testing.T) {
 		t.Run(tt.name, func(subT *testing.T) {
 			hook.Reset()
 			claims := jwt.MapClaims{}
-			if tt.scopeClaim != "" && tt.scope != nil {
-				claims[tt.scopeClaim] = tt.scope
+			if tt.permissionsClaim != "" && tt.permissionsValue != nil {
+				claims[tt.permissionsClaim] = tt.permissionsValue
 			}
-			if tt.rolesClaim != "" && tt.roles != nil {
-				claims[tt.rolesClaim] = tt.roles
+			if tt.rolesClaim != "" && tt.rolesValue != nil {
+				claims[tt.rolesClaim] = tt.rolesValue
 			}
 			tok := jwt.NewWithClaims(signingMethod, claims)
 			pubKeyBytes := []byte("mySecretK3y")
@@ -609,13 +609,13 @@ func Test_JWT_yields_scopes(t *testing.T) {
 
 			source := ac.NewJWTSource("", "Authorization", nil)
 			j, err := ac.NewJWT(&ac.JWTOptions{
-				Algorithm:  algo.String(),
-				Name:       "test_ac",
-				ScopeClaim: tt.scopeClaim,
-				RolesClaim: tt.rolesClaim,
-				RolesMap:   rolesMap,
-				Source:     source,
-				Key:        pubKeyBytes,
+				Algorithm:        algo.String(),
+				Name:             "test_ac",
+				PermissionsClaim: tt.permissionsClaim,
+				RolesClaim:       tt.rolesClaim,
+				RolesMap:         rolesMap,
+				Source:           source,
+				Key:              pubKeyBytes,
 			})
 			if err != nil {
 				subT.Fatal(err)
@@ -629,12 +629,12 @@ func Test_JWT_yields_scopes(t *testing.T) {
 				return
 			}
 
-			scopesList, ok := req.Context().Value(request.Scopes).([]string)
+			grantedPermissionsList, ok := req.Context().Value(request.BetaGrantedPermissions).([]string)
 			if !ok {
-				subT.Errorf("Expected scopes within request context")
+				subT.Errorf("Expected granted permissions within request context")
 			} else {
-				if !reflect.DeepEqual(tt.expScopes, scopesList) {
-					subT.Errorf("Scopes do not match, want: %#v, got: %#v", tt.expScopes, scopesList)
+				if !reflect.DeepEqual(tt.expGrantedPerms, grantedPermissionsList) {
+					subT.Errorf("Granted permissions do not match, want: %#v, got: %#v", tt.expGrantedPerms, grantedPermissionsList)
 				}
 			}
 
diff --git a/accesscontrol/permissions_test.go b/accesscontrol/permissions_test.go
new file mode 100644
index 00000000..32aa9c05
--- /dev/null
+++ b/accesscontrol/permissions_test.go
@@ -0,0 +1,200 @@
+package accesscontrol
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/avenga/couper/config/request"
+	"github.com/avenga/couper/errors"
+)
+
+func Test_requiredPermissions(t *testing.T) {
+	tests := []struct {
+		name string
+		rp   string
+		rpm  map[string]string
+		want map[string]string
+	}{
+		{
+			"no permission (string)",
+			"",
+			nil,
+			nil,
+		},
+		{
+			"no permission for default methods",
+			"",
+			map[string]string{"*": ""},
+			map[string]string{"DELETE": "", "GET": "", "HEAD": "", "OPTIONS": "", "PATCH": "", "POST": "", "PUT": ""},
+		},
+		{
+			"only default read",
+			"",
+			map[string]string{"*": "read"},
+			map[string]string{"DELETE": "read", "GET": "read", "HEAD": "read", "OPTIONS": "read", "PATCH": "read", "POST": "read", "PUT": "read"},
+		},
+		{
+			"simple permission, simple no permission",
+			"",
+			map[string]string{"POST": "write", "PUT": ""},
+			map[string]string{"POST": "write", "PUT": ""},
+		},
+		{
+			"simple permission, simple no permission, with default",
+			"",
+			map[string]string{"POST": "write", "PUT": "", "*": "read"},
+			map[string]string{"DELETE": "read", "GET": "read", "HEAD": "read", "OPTIONS": "read", "PATCH": "read", "POST": "write", "PUT": ""},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(subT *testing.T) {
+			rp := newRequiredPermissions(tt.rp, tt.rpm)
+			if tt.want == nil {
+				if rp.permissions != nil {
+					subT.Errorf("expected permissions to be nil: %#v", rp.permissions)
+				}
+				return
+			}
+			if len(rp.permissions) != len(tt.want) {
+				subT.Errorf("unexpected permission: %#v, want: %#v", rp.permissions, tt.want)
+				return
+			}
+			for method, wantPermission := range tt.want {
+				permission, err := rp.getPermission(method)
+				if err != nil {
+					subT.Errorf("no permission for method %q", method)
+					return
+				}
+				if permission != wantPermission {
+					subT.Errorf("unexpected permission for %q: %#v, want: %#v", method, permission, wantPermission)
+					return
+				}
+			}
+		})
+	}
+}
+
+func Test_PermissionsControl(t *testing.T) {
+	tests := []struct {
+		name               string
+		permission         string
+		permissionMap      map[string]string
+		method             string
+		grantedPermissions []string
+		wantErrorString    string
+	}{
+		{
+			"no method restrictions, no permission required, no permission granted",
+			"",
+			nil,
+			http.MethodGet,
+			nil,
+			"",
+		},
+		{
+			"method permitted, no permission required, no permission granted",
+			"",
+			map[string]string{http.MethodGet: ""},
+			http.MethodGet,
+			nil,
+			"",
+		},
+		{
+			"method permitted, permission required, permission granted",
+			"",
+			map[string]string{http.MethodGet: "read"},
+			http.MethodGet,
+			[]string{"read"},
+			"",
+		},
+		{
+			"method permitted, permission required, permission granted",
+			"",
+			map[string]string{http.MethodPost: "write"},
+			http.MethodPost,
+			[]string{"read", "write"},
+			"",
+		},
+		{
+			"permission required for all methods, permission granted",
+			"read",
+			nil,
+			"BREW",
+			[]string{"read"},
+			"",
+		},
+		{
+			"default methods permitted, permission required, permission granted",
+			"",
+			map[string]string{"*": "read"},
+			http.MethodPost,
+			[]string{"read"},
+			"",
+		},
+		{
+			"default methods permitted, permission required, permission granted but non-default method not allowed",
+			"",
+			map[string]string{"*": "read"},
+			"BREW",
+			[]string{"read"},
+			"method not allowed error: method BREW not allowed by beta_required_permission",
+		},
+		{
+			"standard method not allowed",
+			"",
+			map[string]string{http.MethodGet: ""},
+			http.MethodPost,
+			nil,
+			"method not allowed error: method POST not allowed by beta_required_permission",
+		},
+		{
+			"method permitted, permission required, no permissions granted",
+			"",
+			map[string]string{http.MethodGet: "read"},
+			http.MethodGet,
+			nil,
+			"access control error: no permissions granted",
+		},
+		{
+			"method permitted, permission required, missing required permission",
+			"",
+			map[string]string{http.MethodPost: "write"},
+			http.MethodPost,
+			[]string{"read"},
+			`access control error: required permission "write" not granted`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(subT *testing.T) {
+			pc := NewPermissionsControl(tt.permission, tt.permissionMap)
+			req := httptest.NewRequest(tt.method, "/", nil)
+			if tt.grantedPermissions != nil {
+				ctx := req.Context()
+				ctx = context.WithValue(ctx, request.BetaGrantedPermissions, tt.grantedPermissions)
+				*req = *req.WithContext(ctx)
+			}
+			err := pc.Validate(req)
+			if tt.wantErrorString == "" && err == nil {
+				return
+			}
+			if tt.wantErrorString == "" && err != nil {
+				logErr := err.(errors.GoError)
+				subT.Errorf("no error expected, was: %#q", logErr.LogError())
+				return
+			}
+			if tt.wantErrorString != "" && err == nil {
+				subT.Errorf("no error thrown, expected: %q", tt.wantErrorString)
+				return
+			}
+			logErr := err.(errors.GoError)
+			if tt.wantErrorString != logErr.LogError() {
+				subT.Errorf("unexpected error thrown, expected: %q, was: %q", tt.wantErrorString, logErr.LogError())
+				return
+			}
+		})
+	}
+}
diff --git a/accesscontrol/scope_test.go b/accesscontrol/scope_test.go
deleted file mode 100644
index cf726540..00000000
--- a/accesscontrol/scope_test.go
+++ /dev/null
@@ -1,198 +0,0 @@
-package accesscontrol
-
-import (
-	"context"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/avenga/couper/config/request"
-	"github.com/avenga/couper/errors"
-)
-
-func Test_requiredScope(t *testing.T) {
-	tests := []struct {
-		name string
-		base map[string]string
-		sm   map[string]string
-		want map[string][]string
-	}{
-		{
-			"only default no scope",
-			map[string]string{},
-			map[string]string{"*": ""},
-			map[string][]string{"CONNECT": {}, "DELETE": {}, "GET": {}, "HEAD": {}, "OPTIONS": {}, "PATCH": {}, "POST": {}, "PUT": {}, "TRACE": {}},
-		},
-		{
-			"only default read",
-			map[string]string{},
-			map[string]string{"*": "read"},
-			map[string][]string{"CONNECT": {"read"}, "DELETE": {"read"}, "GET": {"read"}, "HEAD": {"read"}, "OPTIONS": {"read"}, "PATCH": {"read"}, "POST": {"read"}, "PUT": {"read"}, "TRACE": {"read"}},
-		},
-		{
-			"simple scope, simple no scope",
-			map[string]string{},
-			map[string]string{"POST": "write", "PUT": ""},
-			map[string][]string{"POST": {"write"}, "PUT": {}},
-		},
-		{
-			"simple scope, simple no scope, with default",
-			map[string]string{},
-			map[string]string{"POST": "write", "PUT": "", "*": "read"},
-			map[string][]string{"CONNECT": {"read"}, "DELETE": {"read"}, "GET": {"read"}, "HEAD": {"read"}, "OPTIONS": {"read"}, "PATCH": {"read"}, "POST": {"write"}, "PUT": {}, "TRACE": {"read"}},
-		},
-		{
-			"default / simple scope, simple no scope",
-			map[string]string{"*": "read"},
-			map[string]string{"POST": "write", "PUT": ""},
-			map[string][]string{"POST": {"read", "write"}, "PUT": {"read"}},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(subT *testing.T) {
-			rs := newRequiredScope()
-			rs.addScopeMap(tt.base)
-			rs.addScopeMap(tt.sm)
-			if len(rs.scopes) != len(tt.want) {
-				subT.Errorf("unexpected scopes: %#v, want: %#v", rs.scopes, tt.want)
-				return
-			}
-			for op, wantScopes := range tt.want {
-				scopes, exists := rs.scopes[op]
-				if !exists {
-					subT.Errorf("no scopes for operation %q", op)
-					return
-				}
-				if len(scopes) != len(wantScopes) {
-					subT.Errorf("unexpected scopes for %q: %#v, want: %#v", op, scopes, wantScopes)
-					return
-				}
-				for i, s := range wantScopes {
-					if scopes[i] != s {
-						subT.Errorf("unexpected scopes for %q: %#v, want: %#v", op, scopes, wantScopes)
-						return
-					}
-				}
-			}
-		})
-	}
-}
-
-func Test_ScopeControl(t *testing.T) {
-	tests := []struct {
-		name            string
-		scopeMaps       []map[string]string
-		method          string
-		grantedScope    []string
-		wantErrorString string
-	}{
-		{
-			"no method restrictions, no scope required, no scope granted",
-			[]map[string]string{},
-			http.MethodGet,
-			nil,
-			"",
-		},
-		{
-			"method permitted, no scope required, no scope granted",
-			[]map[string]string{{http.MethodGet: ""}},
-			http.MethodGet,
-			nil,
-			"",
-		},
-		{
-			"method permitted, scope required, scope granted",
-			[]map[string]string{{http.MethodGet: "read"}},
-			http.MethodGet,
-			[]string{"read"},
-			"",
-		},
-		{
-			"method permitted, scope required, scopes granted",
-			[]map[string]string{{http.MethodPost: "write"}},
-			http.MethodPost,
-			[]string{"read", "write"},
-			"",
-		},
-		{
-			"method permitted, scopes required, scopes granted",
-			[]map[string]string{{"*": "read"}, {http.MethodPost: "write"}},
-			http.MethodPost,
-			[]string{"read", "write"},
-			"",
-		},
-		{
-			"all methods permitted, scope required, scope granted",
-			[]map[string]string{{"*": "read"}},
-			http.MethodPost,
-			[]string{"read"},
-			"",
-		},
-		{
-			"method not permitted",
-			[]map[string]string{{http.MethodGet: ""}},
-			http.MethodPost,
-			nil,
-			"access control error: operation POST not permitted",
-		},
-		{
-			"method permitted, scope required, no scope granted",
-			[]map[string]string{{http.MethodGet: "read"}},
-			http.MethodGet,
-			nil,
-			"access control error: no scope granted",
-		},
-		{
-			"method permitted, scope required, wrong scope granted",
-			[]map[string]string{{http.MethodPost: "write"}},
-			http.MethodPost,
-			[]string{"read"},
-			`access control error: required scope "write" not granted`,
-		},
-		{
-			"method permitted, scopes required, missing granted scope",
-			[]map[string]string{{"*": "read"}, {http.MethodPost: "write"}},
-			http.MethodPost,
-			[]string{"read"},
-			`access control error: required scope "write" not granted`,
-		},
-		{
-			"method permitted, scopes required, missing granted scopes",
-			[]map[string]string{{"*": "read"}, {http.MethodPost: "write"}},
-			http.MethodPost,
-			[]string{"foo"},
-			`access control error: required scope "read" not granted`,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(subT *testing.T) {
-			sc := NewScopeControl(tt.scopeMaps)
-			req := httptest.NewRequest(tt.method, "/", nil)
-			if tt.grantedScope != nil {
-				ctx := req.Context()
-				ctx = context.WithValue(ctx, request.Scopes, tt.grantedScope)
-				*req = *req.WithContext(ctx)
-			}
-			err := sc.Validate(req)
-			if tt.wantErrorString == "" && err == nil {
-				return
-			}
-			if tt.wantErrorString == "" && err != nil {
-				logErr := err.(errors.GoError)
-				subT.Errorf("no error expected, was: %#q", logErr.LogError())
-				return
-			}
-			if tt.wantErrorString != "" && err == nil {
-				subT.Errorf("no error thrown, expected: %q", tt.wantErrorString)
-				return
-			}
-			logErr := err.(errors.GoError)
-			if tt.wantErrorString != logErr.LogError() {
-				subT.Errorf("unexpected error thrown, expected: %q, was: %q", tt.wantErrorString, logErr.LogError())
-				return
-			}
-		})
-	}
-}
diff --git a/config/configload/validate_test.go b/config/configload/validate_test.go
index 63973b17..3108b34d 100644
--- a/config/configload/validate_test.go
+++ b/config/configload/validate_test.go
@@ -255,18 +255,18 @@ func TestAttributeObjectKeys(t *testing.T) {
 			"couper.hcl:4,30-7,8: key in an attribute must be unique: 'a'; Key must be unique for a.",
 		},
 		{
-			"beta_scope",
+			"beta_required_permission",
 			`server {
   api {
     endpoint "/a" {
-      beta_scope = {
+      beta_required_permission = {
         get = "a"
         GeT = "A"
       }
     }
   }
 }`,
-			"couper.hcl:4,20-7,8: key in an attribute must be unique: 'get'; Key must be unique for get.",
+			"couper.hcl:4,34-7,8: key in an attribute must be unique: 'get'; Key must be unique for get.",
 		},
 		{
 			"headers",
@@ -360,13 +360,13 @@ definitions {
 			"",
 		},
 		{
-			"beta_scope_map",
+			"beta_permissions_map",
 			`server {}
 definitions {
   jwt "a" {
     signature_algorithm = "HS256"
     key = "asdf"
-    beta_scope_map = {
+    beta_permissions_map = {
       a = []
       A = []
 	}
diff --git a/config/runtime/access_control_test.go b/config/runtime/access_control_test.go
index 26f3408b..c27cf675 100644
--- a/config/runtime/access_control_test.go
+++ b/config/runtime/access_control_test.go
@@ -37,26 +37,38 @@ func TestACDefinitions_errors(t *testing.T) {
 			server "test" {
 			}
 			definitions {
-				jwt "scopes" {
+				jwt "beta_granted_permissions" {
 					signature_algorithm = "HS256"
 					key = "$3cRe4"
 					header = "Authorization"
 				}
 			}
 			`,
-			"configuration error: scopes: accessControl uses reserved name as label",
+			"configuration error: beta_granted_permissions: accessControl uses reserved name as label",
 		},
 		{
-			"collision: basic_auth reserved label",
+			"collision: basic_auth reserved label 1",
 			`
 			server "test" {
 			}
 			definitions {
-				basic_auth "scopes" {
+				basic_auth "beta_granted_permissions" {
 				}
 			}
 			`,
-			"configuration error: scopes: accessControl uses reserved name as label",
+			"configuration error: beta_granted_permissions: accessControl uses reserved name as label",
+		},
+		{
+			"collision: basic_auth reserved label 2",
+			`
+			server "test" {
+			}
+			definitions {
+				basic_auth "beta_required_permission" {
+				}
+			}
+			`,
+			"configuration error: beta_required_permission: accessControl uses reserved name as label",
 		},
 		{
 			"jwt with empty label",
diff --git a/server/http_error_handler_test.go b/server/http_error_handler_test.go
index 6ece14ee..dd1b8c05 100644
--- a/server/http_error_handler_test.go
+++ b/server/http_error_handler_test.go
@@ -123,7 +123,7 @@ func TestAccessControl_ErrorHandler_Configuration_Error(t *testing.T) {
 	}
 }
 
-func TestAccessControl_ErrorHandler_Scopes(t *testing.T) {
+func TestAccessControl_ErrorHandler_Permissions(t *testing.T) {
 	client := test.NewHTTPClient()
 
 	helper := test.New(t)
@@ -132,24 +132,24 @@ func TestAccessControl_ErrorHandler_Scopes(t *testing.T) {
 	defer shutdown()
 
 	type testcase struct {
-		Name      string
-		Method    string
-		Path      string
-		Scopes    []string
-		ExpStatus int
-		ExpBody   string
+		Name               string
+		Method             string
+		Path               string
+		GrantedPermissions []string
+		ExpStatus          int
+		ExpBody            string
 	}
 
 	for _, tc := range []testcase{
-		{"api: w/ scope", http.MethodGet, "/api/", []string{"read"}, http.StatusNoContent, ""},
-		{"api: w/ wrong scope; handle scope", http.MethodGet, "/api/", []string{"another"}, http.StatusTeapot, ""},
-		{"api pow: w/ scope; op granted", http.MethodPost, "/api/pow/", []string{"read", "power"}, http.StatusNoContent, ""},
-		{"api pow: w/ wrong scope; handle insufficient_scope", http.MethodPost, "/api/pow/", []string{"read", "another"}, http.StatusBadRequest, ""},
-		{"api pow: w/ scope method; handle operation_denied", http.MethodGet, "/api/pow/", []string{"read", "another"}, http.StatusMethodNotAllowed, ""},
-		{"endpoint: w/ scope", http.MethodGet, "/", []string{"write"}, http.StatusOK, ""},
-		{"endpoint: w/ wrong scope; handle scope", http.MethodGet, "/", []string{"another"}, http.StatusTeapot, ""},
-		{"api specific, endpoint *: w/ wrong scope; handle scope", http.MethodGet, "/wildcard1/", []string{"another"}, http.StatusBadRequest, ""},
-		{"api *, endpoint specific: w/ wrong scope; handle scope", http.MethodGet, "/wildcard2/", []string{"another"}, http.StatusBadRequest, ""},
+		{"api: sufficient permissions", http.MethodGet, "/api/", []string{"read"}, http.StatusNoContent, ""},
+		{"api: insufficient permissions; handle insufficient_permission", http.MethodGet, "/api/", []string{"another"}, http.StatusTeapot, ""},
+		{"api pow: sufficient permissions for method", http.MethodPost, "/api/pow/", []string{"read", "power"}, http.StatusNoContent, ""},
+		{"api pow: insufficient permissions; handle insufficient_permission", http.MethodPost, "/api/pow/", []string{"read", "another"}, http.StatusBadRequest, ""},
+		{"api pow: method not allowed", http.MethodGet, "/api/pow/", []string{"read", "another"}, http.StatusMethodNotAllowed, ""},
+		{"endpoint: sufficient permissions", http.MethodGet, "/", []string{"write"}, http.StatusOK, ""},
+		{"endpoint: insufficient permissions; handle insufficient_permission", http.MethodGet, "/", []string{"another"}, http.StatusTeapot, ""},
+		{"api specific, endpoint *: insufficient permissions; handle insufficient_permission", http.MethodGet, "/wildcard1/", []string{"another"}, http.StatusBadRequest, ""},
+		{"api *, endpoint specific: insufficient permissions; handle insufficient_permission", http.MethodGet, "/wildcard2/", []string{"another"}, http.StatusBadRequest, ""},
 	} {
 		t.Run(tc.Name, func(st *testing.T) {
 			h := test.New(st)
@@ -173,7 +173,7 @@ func TestAccessControl_ErrorHandler_Scopes(t *testing.T) {
 			tokenVal, err := ctx.HCLContext().Functions[lib.FnJWTSign].
 				Call([]cty.Value{cty.StringVal("test"), seetie.MapToValue(
 					map[string]interface{}{
-						"scopes": tc.Scopes,
+						"scope": tc.GrantedPermissions,
 					})})
 			h.Must(err)
 
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index de828cbe..5b920ec8 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -3419,7 +3419,7 @@ func TestJWTAccessControl(t *testing.T) {
 		header     http.Header
 		body       string
 		status     int
-		expScope   string
+		expPerm    string
 		wantErrLog string
 	}
 
@@ -3505,8 +3505,8 @@ func TestJWTAccessControl(t *testing.T) {
 				return
 			}
 
-			if scopes := res.Header.Get("X-Scopes"); scopes != tc.expScope {
-				subT.Errorf("expected scope: %q, actual: %q", tc.expScope, scopes)
+			if grantedPermissions := res.Header.Get("X-Granted-Permissions"); grantedPermissions != tc.expPerm {
+				subT.Errorf("expected granted permissions: %q, actual: %q", tc.expPerm, grantedPermissions)
 				return
 			}
 		})
@@ -3678,7 +3678,7 @@ func getAccessControlMessages(hook *logrustest.Hook) string {
 	return ""
 }
 
-func Test_Scope(t *testing.T) {
+func Test_Permissions(t *testing.T) {
 	h := test.New(t)
 	client := newClient()
 
@@ -3693,37 +3693,39 @@ func Test_Scope(t *testing.T) {
 	h.Must(tokenErr)
 
 	type testCase struct {
-		name        string
-		operation   string
-		path        string
-		authorize   bool
-		status      int
-		wantGranted string
-		wantErrLog  string
-		wantErrType string
+		name         string
+		method       string
+		path         string
+		authorize    bool
+		status       int
+		wantGranted  string
+		wantRequired string
+		wantErrLog   string
+		wantErrType  string
 	}
 
 	for _, tc := range []testCase{
-		{"by scope: unauthorized", http.MethodGet, "/scope/foo", false, http.StatusUnauthorized, ``, "access control error: scoped_jwt: token required", "jwt_token_missing"},
-		{"by scope: sufficient scope", http.MethodGet, "/scope/foo", true, http.StatusNoContent, `["a"]`, "", ""},
-		{"by scope: additional scope required: insufficient scope", http.MethodPost, "/scope/foo", true, http.StatusForbidden, ``, `access control error: required scope "foo" not granted`, "beta_insufficient_scope"},
-		{"by scope: operation not permitted", http.MethodDelete, "/scope/foo", true, http.StatusForbidden, ``, "access control error: operation DELETE not permitted", "beta_operation_denied"},
-		{"by scope: additional scope required by *: insufficient scope", http.MethodGet, "/scope/bar", true, http.StatusForbidden, ``, `access control error: required scope "more" not granted`, "beta_insufficient_scope"},
-		{"by scope: no additional scope required: sufficient scope", http.MethodDelete, "/scope/bar", true, http.StatusNoContent, `["a"]`, "", ""},
-		{"by role: unauthorized", http.MethodGet, "/role/foo", false, http.StatusUnauthorized, ``, "access control error: roled_jwt: token required", "jwt_token_missing"},
-		{"by role: sufficient scope", http.MethodGet, "/role/foo", true, http.StatusNoContent, `["a","b"]`, "", ""},
-		{"by role: additional scope required: insufficient scope", http.MethodPost, "/role/foo", true, http.StatusForbidden, ``, `access control error: required scope "foo" not granted`, "beta_insufficient_scope"},
-		{"by role: operation not permitted", http.MethodDelete, "/role/foo", true, http.StatusForbidden, ``, "access control error: operation DELETE not permitted", "beta_operation_denied"},
-		{"by role: additional scope required by *: insufficient scope", http.MethodGet, "/role/bar", true, http.StatusForbidden, ``, `access control error: required scope "more" not granted`, "beta_insufficient_scope"},
-		{"by role: no additional scope required: sufficient scope", http.MethodDelete, "/role/bar", true, http.StatusNoContent, `["a","b"]`, "", ""},
-		{"by scope/role, mapped from scope", http.MethodGet, "/scope_and_role/foo", true, http.StatusNoContent, `["a","b","c","d","e"]`, "", ""},
-		{"by scope/role, mapped scope mapped from role", http.MethodGet, "/scope_and_role/bar", true, http.StatusNoContent, `["a","b","c","d","e"]`, "", ""},
+		{"by scope: unauthorized", http.MethodGet, "/scope/foo", false, http.StatusUnauthorized, ``, ``, "access control error: scoped_jwt: token required", "jwt_token_missing"},
+		{"by scope: no permission required by endpoint", http.MethodGet, "/scope/foo", true, http.StatusNoContent, `["a"]`, ``, "", ""},
+		{"by scope: permission required by endpoint: insufficient permissions", http.MethodPost, "/scope/foo", true, http.StatusForbidden, ``, ``, `access control error: required permission "foo" not granted`, "beta_insufficient_permissions"},
+		{"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 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"]`, ``, "", ""},
+		{"by role: permission required by endpoint: insufficient permissions", http.MethodPost, "/role/foo", true, http.StatusForbidden, ``, ``, `access control error: required permission "foo" not granted`, "beta_insufficient_permissions"},
+		{"by role: method not permitted", http.MethodDelete, "/role/foo", true, http.StatusMethodNotAllowed, ``, ``, "method not allowed error: method DELETE not allowed by beta_required_permission", ""},
+		{"by role: permission required by endpoint via *: insufficient permissions", http.MethodGet, "/role/bar", true, http.StatusForbidden, ``, `more`, `access control error: required permission "more" not granted`, "beta_insufficient_permissions"},
+		{"by role: no permission required by endpoint", http.MethodDelete, "/role/bar", true, http.StatusNoContent, `["a","b"]`, ``, "", ""},
+		{"by scope/role, mapped from scope", http.MethodGet, "/scope_and_role/foo", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""},
+		{"by scope/role, mapped scope mapped from role", http.MethodGet, "/scope_and_role/bar", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""},
 	} {
-		t.Run(fmt.Sprintf("%s_%s_%s", tc.name, tc.operation, tc.path), func(subT *testing.T) {
+		t.Run(fmt.Sprintf("%s_%s_%s", tc.name, tc.method, tc.path), func(subT *testing.T) {
 			helper := test.New(subT)
 			hook.Reset()
 
-			req, err := http.NewRequest(tc.operation, "http://back.end:8080"+tc.path, nil)
+			req, err := http.NewRequest(tc.method, "http://back.end:8080"+tc.path, nil)
 			helper.Must(err)
 
 			if tc.authorize {
@@ -3737,9 +3739,14 @@ func Test_Scope(t *testing.T) {
 				subT.Fatalf("expected Status %d, got: %d", tc.status, res.StatusCode)
 			}
 
-			granted := res.Header.Get("x-granted-scope")
+			granted := res.Header.Get("x-granted-permissions")
 			if granted != tc.wantGranted {
-				subT.Errorf("Expected granted scope:\nWant:\t%q\nGot:\t%q", tc.wantGranted, granted)
+				subT.Errorf("Expected granted permissions:\nWant:\t%q\nGot:\t%q", tc.wantGranted, granted)
+			}
+
+			required := res.Header.Get("x-required-permission")
+			if required != tc.wantRequired {
+				subT.Errorf("Expected required permission:\nWant:\t%q\nGot:\t%q", tc.wantRequired, required)
 			}
 
 			message := getAccessControlMessages(hook)
@@ -4999,8 +5006,8 @@ func TestAllowedMethods(t *testing.T) {
 		{"unrestricted, authorized, POST", http.MethodPost, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
 		{"unrestricted, authorized, PUT", http.MethodPut, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
 		{"unrestricted, authorized, PATCH", http.MethodPatch, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
-		{"unrestricted, authorized, DELETE", http.MethodDelete, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusForbidden, "access control error"},
-		{"unrestricted, authorized, OPTIONS", http.MethodOptions, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusForbidden, "access control error"},
+		{"unrestricted, authorized, DELETE", http.MethodDelete, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
+		{"unrestricted, authorized, OPTIONS", http.MethodOptions, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
 		{"unrestricted, authorized, CONNECT", http.MethodConnect, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
 		{"unrestricted, authorized, TRACE", http.MethodTrace, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
 		{"unrestricted, authorized, BREW", "BREW", "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
@@ -5013,11 +5020,11 @@ func TestAllowedMethods(t *testing.T) {
 		{"restricted, authorized, POST", http.MethodPost, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
 		{"restricted, authorized, PUT", http.MethodPut, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
 		{"restricted, authorized, PATCH", http.MethodPatch, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
-		{"restricted, authorized, DELETE", http.MethodDelete, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusForbidden, "access control error"},
+		{"restricted, authorized, DELETE", http.MethodDelete, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
 		{"restricted, authorized, OPTIONS", http.MethodOptions, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
 		{"restricted, authorized, CONNECT", http.MethodConnect, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
 		{"restricted, authorized, TRACE", http.MethodTrace, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"},
-		{"restricted, authorized, BREW", "BREW", "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusForbidden, "access control error"}, // BREW not supported by scope AC
+		{"restricted, authorized, BREW", "BREW", "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""},
 		{"restricted, CORS preflight", http.MethodOptions, "/api1/restricted", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""},
 
 		{"wildcard, GET", http.MethodGet, "/api1/wildcard", http.Header{}, http.StatusOK, ""},
diff --git a/server/testdata/integration/config/03_couper.hcl b/server/testdata/integration/config/03_couper.hcl
index 8951bd96..0ba67000 100644
--- a/server/testdata/integration/config/03_couper.hcl
+++ b/server/testdata/integration/config/03_couper.hcl
@@ -108,7 +108,7 @@ server "acs" {
     response {
       headers = {
         x-jwt-sub = request.context.JWTToken.sub
-        x-scopes = json_encode(request.context.scopes)
+        x-granted-permissions = json_encode(request.context.beta_granted_permissions)
       }
     }
   }
@@ -119,7 +119,7 @@ server "acs" {
     response {
       headers = {
         x-jwt-sub = request.context.JWT_token_value_query.sub
-        x-scopes = json_encode(request.context.scopes)
+        x-granted-permissions = json_encode(request.context.beta_granted_permissions)
       }
     }
   }
@@ -130,7 +130,7 @@ server "acs" {
     response {
       headers = {
         x-jwt-sub = request.context.JWT_token_value_body.sub
-        x-scopes = json_encode(request.context.scopes)
+        x-granted-permissions = json_encode(request.context.beta_granted_permissions)
       }
     }
   }
@@ -231,7 +231,7 @@ server "acs" {
     response {
       headers = {
         x-jwt-sub = request.context.JWKS_scope.sub
-        x-scopes = json_encode(request.context.scopes)
+        x-granted-permissions = json_encode(request.context.beta_granted_permissions)
       }
     }
   }
@@ -291,7 +291,7 @@ definitions {
   jwt "JWTToken" {
     signature_algorithm = "HS256"
     key = "y0urS3cretT08eU5edF0rC0uPerInThe3xamp1e"
-    beta_scope_claim = "scope"
+    beta_permissions_claim = "scope"
   }
   jwt "RSAToken" {
     signature_algorithm = "RS256"
@@ -353,7 +353,7 @@ definitions {
   }
   jwt "JWKS_scope" {
     jwks_url = "file:../files/jwks.json"
-    beta_scope_claim = "scope"
+    beta_permissions_claim = "scope"
   }
   jwt "JWKSRemote" {
     jwks_url = "${env.COUPER_TEST_BACKEND_ADDR}/jwks.json"
@@ -373,13 +373,13 @@ definitions {
     token_value = request.query.token[0]
     signature_algorithm = "HS256"
     key = "y0urS3cretT08eU5edF0rC0uPerInThe3xamp1e"
-    beta_scope_claim = "scope"
+    beta_permissions_claim = "scope"
   }
   jwt "JWT_token_value_body" {
     token_value = request.json_body.token
     signature_algorithm = "HS256"
     key = "y0urS3cretT08eU5edF0rC0uPerInThe3xamp1e"
-    beta_scope_claim = "scope"
+    beta_permissions_claim = "scope"
   }
   backend "jwks" {
     origin = env.COUPER_TEST_BACKEND_ADDR
diff --git a/server/testdata/integration/config/09_couper.hcl b/server/testdata/integration/config/09_couper.hcl
index 15325227..449c58d3 100644
--- a/server/testdata/integration/config/09_couper.hcl
+++ b/server/testdata/integration/config/09_couper.hcl
@@ -2,28 +2,44 @@ server "scoped jwt" {
   api {
     base_path = "/scope"
     access_control = ["scoped_jwt"]
-    beta_scope = "a"
+    beta_required_permission = "z"
     endpoint "/foo" {
-      beta_scope = {
+      beta_required_permission = {
         get = ""
         post = "foo"
       }
       response {
         status = 204
         headers = {
-          x-granted-scope = json_encode(request.context.scopes)
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
         }
       }
     }
     endpoint "/bar" {
-      beta_scope = {
+      beta_required_permission = {
         delete = ""
         "*" = "more"
       }
+      error_handler "beta_insufficient_permissions" {
+        response {
+          status = 403
+          headers = {
+            x-required-permission = request.context.beta_required_permission
+          }
+        }
+      }
+      response {
+        status = 204
+        headers = {
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
+        }
+      }
+    }
+    endpoint "/permission-from-api" {
       response {
         status = 204
         headers = {
-          x-granted-scope = json_encode(request.context.scopes)
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
         }
       }
     }
@@ -31,28 +47,36 @@ server "scoped jwt" {
   api {
     base_path = "/role"
     access_control = ["roled_jwt"]
-    beta_scope = "a"
+    beta_required_permission = "a"
     endpoint "/foo" {
-      beta_scope = {
+      beta_required_permission = {
         get = ""
         post = "foo"
       }
       response {
         status = 204
         headers = {
-          x-granted-scope = json_encode(request.context.scopes)
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
         }
       }
     }
     endpoint "/bar" {
-      beta_scope = {
+      beta_required_permission = {
         delete = ""
         "*" = "more"
       }
+      error_handler "beta_insufficient_permissions" {
+        response {
+          status = 403
+          headers = {
+            x-required-permission = request.context.beta_required_permission
+          }
+        }
+      }
       response {
         status = 204
         headers = {
-          x-granted-scope = json_encode(request.context.scopes)
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
         }
       }
     }
@@ -61,20 +85,20 @@ server "scoped jwt" {
     base_path = "/scope_and_role"
     access_control = ["scoped_and_roled_jwt"]
     endpoint "/foo" {
-      beta_scope = "d"
+      beta_required_permission = "d"
       response {
         status = 204
         headers = {
-          x-granted-scope = json_encode(request.context.scopes)
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
         }
       }
     }
     endpoint "/bar" {
-      beta_scope = "e"
+      beta_required_permission = "e"
       response {
         status = 204
         headers = {
-          x-granted-scope = json_encode(request.context.scopes)
+          x-granted-permissions = json_encode(request.context.beta_granted_permissions)
         }
       }
     }
@@ -85,7 +109,7 @@ definitions {
     header = "authorization"
     signature_algorithm = "HS256"
     key = "asdf"
-    beta_scope_claim = "scp"
+    beta_permissions_claim = "scp"
   }
   jwt "roled_jwt" {
     header = "authorization"
@@ -100,14 +124,14 @@ definitions {
     header = "authorization"
     signature_algorithm = "HS256"
     key = "asdf"
-    beta_scope_claim = "scp"
+    beta_permissions_claim = "scp"
     beta_roles_claim = "rl"
     beta_roles_map = {
       "r1" = ["b"]
     }
-    beta_scope_map = {
+    beta_permissions_map = {
       a = ["c"]
-      b = ["e"] # from role-mapped scope
+      b = ["e"] # from role-mapped permission
       c = ["d"]
       d = ["a"] # cycle is ignored
     }
diff --git a/server/testdata/integration/config/11_couper.hcl b/server/testdata/integration/config/11_couper.hcl
index 311e38c3..c3c02e5e 100644
--- a/server/testdata/integration/config/11_couper.hcl
+++ b/server/testdata/integration/config/11_couper.hcl
@@ -25,7 +25,7 @@ server {
 
     endpoint "/unrestricted" {
       access_control = ["token"]
-      beta_scope = {
+      beta_required_permission = {
         get = "foo"
         head = "foo"
         post = "foo"
@@ -42,7 +42,7 @@ server {
     endpoint "/restricted" {
       access_control = ["token"]
       allowed_methods = ["GET", "Post", "delete", "BREW"]
-      beta_scope = {
+      beta_required_permission = {
         get = "foo"
         head = "foo"
         post = "foo"
@@ -67,6 +67,7 @@ server {
 
     endpoint "/wildcardAndMore" {
       allowed_methods = ["get", "*", "PuT", "brew"]
+      beta_required_permission = ""
 
       response {
         body = "a"
@@ -106,6 +107,6 @@ definitions {
   jwt "token" {
     signature_algorithm = "HS256"
     key = "asdf"
-    beta_scope_claim = "scope"
+    beta_permissions_claim = "scope"
   }
 }
diff --git a/server/testdata/integration/endpoint_eval/20_couper.hcl b/server/testdata/integration/endpoint_eval/20_couper.hcl
index 3ffad8f8..2e963a18 100644
--- a/server/testdata/integration/endpoint_eval/20_couper.hcl
+++ b/server/testdata/integration/endpoint_eval/20_couper.hcl
@@ -181,7 +181,7 @@ server "cty.NilVal" {
       headers = {
         X-Value = json_encode(merge({ "obj": {"key": "val"} }, {
           foo = "bar"
-          xxxx = request.context.scopes
+          xxxx = request.context.beta_granted_permissions
         }))
         Z-Value = "y"
       }
diff --git a/server/testdata/integration/error_handler/04_couper.hcl b/server/testdata/integration/error_handler/04_couper.hcl
index f3566111..1dfbf63f 100644
--- a/server/testdata/integration/error_handler/04_couper.hcl
+++ b/server/testdata/integration/error_handler/04_couper.hcl
@@ -5,7 +5,7 @@ server "scoped" {
   api {
     base_path = "/api"
 
-    beta_scope = "read"
+    beta_required_permission = "read"
     endpoint "/" {
       response {
         status = 204
@@ -13,7 +13,7 @@ server "scoped" {
     }
 
     endpoint "/pow/" {
-      beta_scope = {
+      beta_required_permission = {
         post = "power"
       }
 
@@ -21,22 +21,15 @@ server "scoped" {
         status = 204
       }
 
-      error_handler "beta_insufficient_scope" {
+      error_handler "beta_insufficient_permissions" {
         response {
           status = 400
           body = "Not enough power"
         }
       }
-
-      error_handler "beta_operation_denied" {
-        response {
-          status = 405
-          body = "Not enough power"
-        }
-      }
     }
 
-    error_handler "beta_insufficient_scope" {
+    error_handler "beta_insufficient_permissions" {
       response {
         status = 418
       }
@@ -47,7 +40,7 @@ server "scoped" {
   api {
     base_path = "/wildcard1"
 
-    error_handler "beta_insufficient_scope" {
+    error_handler "beta_insufficient_permissions" {
       response {
         status = 418
         body = "Not enough power"
@@ -55,7 +48,7 @@ server "scoped" {
     }
 
     endpoint "/" {
-      beta_scope = "power"
+      beta_required_permission = "power"
 
       response {
         status = 204
@@ -81,13 +74,13 @@ server "scoped" {
     }
 
     endpoint "/" {
-      beta_scope = "power"
+      beta_required_permission = "power"
 
       response {
         status = 204
       }
 
-      error_handler "beta_insufficient_scope" {
+      error_handler "beta_insufficient_permissions" {
         response {
           status = 400
           body = "Not enough power"
@@ -97,13 +90,13 @@ server "scoped" {
   }
 
   endpoint "/" {
-    beta_scope = "write"
+    beta_required_permission = "write"
 
     response {
       body = "OK"
     }
 
-    error_handler "beta_scope" {
+    error_handler "beta_insufficient_permissions" {
       response {
         status = 418
       }
@@ -117,6 +110,6 @@ definitions {
     header = "Authorization"
     signature_algorithm = "HS256"
     key = "s3cr3t"
-    beta_scope_claim = "scopes"
+    beta_permissions_claim = "scope"
   }
 }
diff --git a/server/testdata/integration/logs/01_couper.hcl b/server/testdata/integration/logs/01_couper.hcl
index a1973269..a7d0e5de 100644
--- a/server/testdata/integration/logs/01_couper.hcl
+++ b/server/testdata/integration/logs/01_couper.hcl
@@ -154,13 +154,13 @@ server "logs" {
     endpoint "/error-handler/endpoint" {
       access_control = ["JWT"]
 
-      beta_scope = "required"
+      beta_required_permission = "required"
 
       response {
         status = 204
       }
 
-      error_handler "beta_insufficient_scope" {
+      error_handler "beta_insufficient_permissions" {
         custom_log_fields = {
           error_handler = request.method
         }
diff --git a/server/testdata/multi/server/couper.d/couper.hcl b/server/testdata/multi/server/couper.d/couper.hcl
index f736aeed..17c4042d 100644
--- a/server/testdata/multi/server/couper.d/couper.hcl
+++ b/server/testdata/multi/server/couper.d/couper.hcl
@@ -20,10 +20,10 @@ server {
 
   api "ERR" {
     access_control = ["scoped"]
-    beta_scope = {
+    beta_required_permission = {
       GET = "gimme"
     }
-    error_handler "beta_insufficient_scope" "unexpected_status" "*" {
+    error_handler "beta_insufficient_permissions" "unexpected_status" "*" {
       response {
         status = 418
       }
@@ -34,7 +34,7 @@ server {
     base_path      = "/api-111"
     access_control = ["foo"]
 
-    error_handler "beta_insufficient_scope" {
+    error_handler "beta_insufficient_permissions" {
       response {
         status = 415
       }
diff --git a/server/testdata/multi/server/couper.hcl b/server/testdata/multi/server/couper.hcl
index 337dda78..4528b99f 100644
--- a/server/testdata/multi/server/couper.hcl
+++ b/server/testdata/multi/server/couper.hcl
@@ -29,7 +29,7 @@ server {
       }
     }
 
-    error_handler "unexpected_status * beta_insufficient_scope" {
+    error_handler "unexpected_status * beta_insufficient_permissions" {
       response {
         status = 405
       }
@@ -39,7 +39,7 @@ server {
   api "API" {
     base_path = "/api-1"
 
-    error_handler "beta_insufficient_scope" {
+    error_handler "beta_insufficient_permissions" {
       response {
         status = 401
       }