Skip to content

Commit

Permalink
beta_scope attribute for api and endpoint block provides required sco…
Browse files Browse the repository at this point in the history
…pe checked by scope access control
  • Loading branch information
Johannes Koch committed Sep 16, 2021
1 parent 870ce61 commit d90aa5e
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 5 deletions.
3 changes: 3 additions & 0 deletions config/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/zclconf/go-cty/cty"
)

var _ Inline = &API{}
Expand All @@ -16,6 +17,8 @@ type API struct {
Endpoints Endpoints `hcl:"endpoint,block"`
ErrorFile string `hcl:"error_file,optional"`
Remain hcl.Body `hcl:",remain"`
Scope cty.Value `hcl:"beta_scope,optional"`

// internally used
CatchAllEndpoint *Endpoint
}
Expand Down
2 changes: 2 additions & 0 deletions config/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/zclconf/go-cty/cty"

"github.com/avenga/couper/config/meta"
)
Expand All @@ -18,6 +19,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"`

// internally configured due to multi-label options
Proxies Proxies
Expand Down
27 changes: 22 additions & 5 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
memStore: memStore,
proxyFromEnv: conf.Settings.NoProxyFromEnv,
srvOpts: serverOptions,
},
log)
}, nil, log)

if err != nil {
return nil, err
Expand Down Expand Up @@ -203,7 +202,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
memStore: memStore,
proxyFromEnv: conf.Settings.NoProxyFromEnv,
srvOpts: serverOptions,
}, log)
}, nil, log)

if err != nil {
return nil, err
Expand Down Expand Up @@ -267,6 +266,20 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
if parentAPI != nil && parentAPI.CatchAllEndpoint == endpointConf {
protectedHandler = epOpts.Error.ServeError(errors.RouteNotFound)
}
scopeMaps := []map[string]string{}
if parentAPI != nil {
apiScopeMap, err := seetie.ValueToScopeMap(parentAPI.Scope)
if err != nil {
return nil, err
}
scopeMaps = append(scopeMaps, apiScopeMap)
}
endpointScopeMap, err := seetie.ValueToScopeMap(endpointConf.Scope)
if err != nil {
return nil, err
}
scopeMaps = append(scopeMaps, endpointScopeMap)
scopeControl := ac.NewScopeControl(scopeMaps)
endpointHandlers[endpointConf], err = configureProtectedHandler(accessControls, confCtx, accessControl,
config.NewAccessControl(endpointConf.AccessControl, endpointConf.DisableAccessControl),
&protectedOptions{
Expand All @@ -275,7 +288,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
memStore: memStore,
proxyFromEnv: conf.Settings.NoProxyFromEnv,
srvOpts: serverOptions,
}, log)
}, scopeControl, log)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -562,7 +575,7 @@ type protectedOptions struct {
}

func configureProtectedHandler(m ACDefinitions, ctx *hcl.EvalContext, parentAC, handlerAC config.AccessControl,
opts *protectedOptions, log *logrus.Entry) (http.Handler, error) {
opts *protectedOptions, scopeControl *ac.ScopeControl, log *logrus.Entry) (http.Handler, error) {
var list ac.List
for _, acName := range parentAC.Merge(handlerAC).List() {
if e := m.MustExist(acName); e != nil {
Expand All @@ -573,6 +586,10 @@ func configureProtectedHandler(m ACDefinitions, ctx *hcl.EvalContext, parentAC,
ac.NewItem(acName, m[acName].Control, newErrorHandler(ctx, opts, log, m, acName)),
)
}
if scopeControl != nil {
// TODO properly create error handler
list = append(list, ac.NewItem("scope", scopeControl, handler.NewErrorHandler(nil, opts.epOpts.Error)))
}

if len(list) > 0 {
return handler.NewAccessControl(opts.handler, list), nil
Expand Down
22 changes: 22 additions & 0 deletions internal/seetie/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ func ValueToMap(val cty.Value) map[string]interface{} {
return result
}

func ValueToScopeMap(val cty.Value) (map[string]string, error) {
scopeMap := make(map[string]string)
switch val.Type() {
case cty.NilType:
return nil, nil
case cty.String:
scopeMap["*"] = val.AsString()
return scopeMap, nil
default:
if val.Type().IsObjectType() {
for k, v := range val.AsValueMap() {
if v.Type() != cty.String {
return nil, fmt.Errorf("unsupported value for operation %q in beta_scope", k)
}
scopeMap[strings.ToUpper(k)] = v.AsString()
}
return scopeMap, nil
}
}
return nil, fmt.Errorf("unsupported value for beta_scope")
}

func ValuesMapToValue(m url.Values) cty.Value {
result := make(map[string]interface{})
for k, v := range m {
Expand Down
79 changes: 79 additions & 0 deletions server/http_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"text/template"
"time"

"github.com/dgrijalva/jwt-go/v4"
"github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"

Expand Down Expand Up @@ -2860,6 +2861,72 @@ func getAccessControlMessages(hook *logrustest.Hook) string {
return ""
}

func Test_Scope(t *testing.T) {
h := test.New(t)
client := newClient()

shutdown, hook := newCouper("testdata/integration/config/09_couper.hcl", test.New(t))
defer shutdown()

tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"scp": "a",
})
token, tokenErr := tok.SignedString([]byte("asdf"))
h.Must(tokenErr)

type testCase struct {
name string
operation string
path string
authorize bool
status int
wantErrLog string
wantErrType string
}

for _, tc := range []testCase{
{"GET, unauthorized", http.MethodGet, "/foo", false, http.StatusUnauthorized, "access control error: myjwt: token required", "jwt_token_missing"},
{"GET, sufficient scope", http.MethodGet, "/foo", true, http.StatusNoContent, "", ""},
{"POST, insufficient scope", http.MethodPost, "/foo", true, http.StatusForbidden, `access control error: scope: required scope "foo" not granted`, "insufficient_scope"},
{"DELETE, operation not permitted", http.MethodDelete, "/foo", true, http.StatusForbidden, "access control error: scope: operation DELETE not permitted", "operation_denied"},
{"GET, insufficient scope", http.MethodGet, "/bar", true, http.StatusForbidden, `access control error: scope: required scope "more" not granted`, "insufficient_scope"},
} {
t.Run(tc.name, func(subT *testing.T) {
helper := test.New(subT)
hook.Reset()

req, err := http.NewRequest(tc.operation, "http://back.end:8080"+tc.path, nil)
if tc.authorize {
req.Header.Set("Authorization", "Bearer "+token)
}
helper.Must(err)

res, err := client.Do(req)
helper.Must(err)

if res.StatusCode != tc.status {
t.Errorf("%q: expected Status %d, got: %d", tc.name, tc.status, res.StatusCode)
return
}

message := getAccessControlMessages(hook)
if tc.wantErrLog == "" {
if message != "" {
t.Errorf("Expected error log: %q, actual: %#v", tc.wantErrLog, message)
}
} else {
if !strings.HasPrefix(message, tc.wantErrLog) {
t.Errorf("Expected error log message: %q, actual: %#v", tc.wantErrLog, message)
}
errorType := getAccessLogErrorType(hook)
if errorType != tc.wantErrType {
t.Errorf("Expected error type: %q, actual: %q", tc.wantErrType, errorType)
}
}
})
}
}

func getAccessLogUrl(hook *logrustest.Hook) string {
for _, entry := range hook.AllEntries() {
if entry.Data["type"] == "couper_access" && entry.Data["url"] != "" {
Expand All @@ -2872,6 +2939,18 @@ func getAccessLogUrl(hook *logrustest.Hook) string {
return ""
}

func getAccessLogErrorType(hook *logrustest.Hook) string {
for _, entry := range hook.AllEntries() {
if entry.Data["type"] == "couper_access" && entry.Data["error_type"] != "" {
if errorType, ok := entry.Data["error_type"].(string); ok {
return errorType
}
}
}

return ""
}

func TestWrapperHiJack_WebsocketUpgrade(t *testing.T) {
helper := test.New(t)
shutdown, _ := newCouper("testdata/integration/api/04_couper.hcl", test.New(t))
Expand Down
31 changes: 31 additions & 0 deletions server/testdata/integration/config/09_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
server "scoped jwt" {
api {
access_control = ["myjwt"]
beta_scope = "a"
endpoint "/foo" {
beta_scope = {
get = ""
post = "foo"
}
response {
status = 204
}
}
endpoint "/bar" {
beta_scope = {
"*" = "more"
}
response {
status = 204
}
}
}
}
definitions {
jwt "myjwt" {
header = "authorization"
signature_algorithm = "HS256"
key = "asdf"
beta_scope_claim = "scp"
}
}

0 comments on commit d90aa5e

Please sign in to comment.