Skip to content

Commit

Permalink
rule: Adds validator for rules (#77)
Browse files Browse the repository at this point in the history
This patch adds an input validator for rules which should prevent
accidental typos or similar issues when creating a rule. Additionally,
no invalid/unconfigured handlers (authorizers, credential issuers,
authenticators) can be used.
  • Loading branch information
arekkas authored Jun 15, 2018
1 parent 4f2c64b commit f450697
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 44 deletions.
3 changes: 2 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 78 additions & 0 deletions cmd/helper_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import (
"strings"
"time"

"github.com/ory/fosite"
"github.com/ory/hydra/sdk/go/hydra"
"github.com/ory/keto/sdk/go/keto"
"github.com/ory/oathkeeper/proxy"
"github.com/ory/oathkeeper/rsakey"
"github.com/ory/oathkeeper/rule"
"github.com/pkg/errors"
Expand Down Expand Up @@ -118,3 +121,78 @@ func keyManagerFactory(l logrus.FieldLogger) (keyManager rsakey.Manager, err err

return keyManager, nil
}

func availableHandlerNames() ([]string, []string, []string) {
return []string{
new(proxy.AuthenticatorNoOp).GetID(),
new(proxy.AuthenticatorAnonymous).GetID(),
new(proxy.AuthenticatorOAuth2Introspection).GetID(),
new(proxy.AuthenticatorOAuth2ClientCredentials).GetID(),
},
[]string{
new(proxy.AuthorizerAllow).GetID(),
new(proxy.AuthorizerDeny).GetID(),
new(proxy.AuthorizerKetoWarden).GetID(),
},
[]string{
new(proxy.CredentialsIssuerNoOp).GetID(),
new(proxy.CredentialsIDToken).GetID(),
}
}

func enabledHandlerNames() (d []string, e []string, f []string) {
a, b, c := handlerFactories(nil)
for _, i := range a {
d = append(d, i.GetID())
}
for _, i := range b {
e = append(e, i.GetID())
}
for _, i := range c {
f = append(f, i.GetID())
}
return
}

func handlerFactories(keyManager rsakey.Manager) ([]proxy.Authenticator, []proxy.Authorizer, []proxy.CredentialsIssuer) {
var authorizers = []proxy.Authorizer{
proxy.NewAuthorizerAllow(),
proxy.NewAuthorizerDeny(),
}

if u := viper.GetString("AUTHORIZER_KETO_WARDEN_KETO_URL"); len(u) > 0 {
ketoSdk, err := keto.NewCodeGenSDK(&keto.Configuration{
EndpointURL: viper.GetString("AUTHORIZER_KETO_WARDEN_KETO_URL"),
})
if err != nil {
logger.WithError(err).Fatal("Unable to initialize the ORY Keto SDK")
}
authorizers = append(authorizers, proxy.NewAuthorizerKetoWarden(ketoSdk))
}

return []proxy.Authenticator{
proxy.NewAuthenticatorNoOp(),
proxy.NewAuthenticatorAnonymous(viper.GetString("AUTHENTICATOR_ANONYMOUS_USERNAME")),
proxy.NewAuthenticatorOAuth2Introspection(
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID"),
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_SECRET"),
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL"),
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL"),
strings.Split(viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE"), ","),
fosite.WildcardScopeStrategy,
),
proxy.NewAuthenticatorOAuth2ClientCredentials(
viper.GetString("AUTHENTICATOR_OAUTH2_CLIENT_CREDENTIALS_TOKEN_URL"),
),
},
authorizers,
[]proxy.CredentialsIssuer{
proxy.NewCredentialsIssuerNoOp(),
proxy.NewCredentialsIssuerIDToken(
keyManager,
logger,
viper.GetDuration("CREDENTIALS_ISSUER_ID_TOKEN_LIFESPAN"),
viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_ISSUER"),
),
}
}
9 changes: 8 additions & 1 deletion cmd/serve_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,15 @@ HTTP CONTROLS
logger.WithError(err).Fatalln("Unable to initialize the ID Token signing algorithm")
}

enabledAuthenticators, enabledAuthorizers, enabledCredentialIssuers := enabledHandlerNames()
availableAuthenticators, availableAuthorizers, availableCredentialIssuers := availableHandlerNames()

writer := herodot.NewJSONWriter(logger)
ruleHandler := rule.NewHandler(writer, rules)
ruleHandler := rule.NewHandler(writer, rules, rule.ValidateRule(
enabledAuthenticators, availableAuthenticators,
enabledAuthorizers, availableAuthorizers,
enabledCredentialIssuers, availableCredentialIssuers,
))
keyHandler := rsakey.NewHandler(writer, keyManager)
router := httprouter.New()
ruleHandler.SetRoutes(router)
Expand Down
33 changes: 2 additions & 31 deletions cmd/serve_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ import (
"net/http"
"net/http/httputil"

"strings"

"github.com/meatballhat/negroni-logrus"
"github.com/ory/fosite"
"github.com/ory/go-convenience/corsx"
"github.com/ory/graceful"
"github.com/ory/keto/sdk/go/keto"
Expand Down Expand Up @@ -180,34 +177,8 @@ OTHER CONTROLS
authorizers = append(authorizers, proxy.NewAuthorizerKetoWarden(ketoSdk))
}

eval := proxy.NewRequestHandler(
logger,
[]proxy.Authenticator{
proxy.NewAuthenticatorNoOp(),
proxy.NewAuthenticatorAnonymous(viper.GetString("AUTHENTICATOR_ANONYMOUS_USERNAME")),
proxy.NewAuthenticatorOAuth2Introspection(
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_ID"),
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_CLIENT_SECRET"),
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_TOKEN_URL"),
viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_INTROSPECT_URL"),
strings.Split(viper.GetString("AUTHENTICATOR_OAUTH2_INTROSPECTION_SCOPE"), ","),
fosite.WildcardScopeStrategy,
),
proxy.NewAuthenticatorOAuth2ClientCredentials(
viper.GetString("AUTHENTICATOR_OAUTH2_CLIENT_CREDENTIALS_TOKEN_URL"),
),
},
authorizers,
[]proxy.CredentialsIssuer{
proxy.NewCredentialsIssuerNoOp(),
proxy.NewCredentialsIssuerIDToken(
keyManager,
logger,
viper.GetDuration("CREDENTIALS_ISSUER_ID_TOKEN_LIFESPAN"),
viper.GetString("CREDENTIALS_ISSUER_ID_TOKEN_ISSUER"),
),
},
)
authenticators, authorizers, credentialIssuers := handlerFactories(keyManager)
eval := proxy.NewRequestHandler(logger, authenticators, authorizers, credentialIssuers)
d := proxy.NewProxy(eval, logger, matcher)
handler := &httputil.ReverseProxy{
Director: d.Director,
Expand Down
5 changes: 5 additions & 0 deletions helper/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ var (
CodeField: http.StatusConflict,
StatusField: http.StatusText(http.StatusConflict),
}
ErrBadRequest = &herodot.DefaultError{
ErrorField: "The request is malformed or contains invalid data",
CodeField: http.StatusBadRequest,
StatusField: http.StatusText(http.StatusBadRequest),
}
)
7 changes: 7 additions & 0 deletions rule/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@ import (
type Handler struct {
H herodot.Writer
M Manager
V func(*Rule) error
}

func NewHandler(
h herodot.Writer,
m Manager,
v func(*Rule) error,
) *Handler {
return &Handler{
H: h,
M: m,
V: v,
}
}

Expand Down Expand Up @@ -233,5 +236,9 @@ func (h *Handler) decodeRule(w http.ResponseWriter, r *http.Request) (*Rule, err
return nil, errors.WithStack(err)
}

if err := h.V(rule); err != nil {
return nil, err
}

return rule, nil
}
12 changes: 12 additions & 0 deletions rule/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func TestHandler(t *testing.T) {
handler := &Handler{
H: herodot.NewJSONWriter(nil),
M: NewMemoryManager(),
V: ValidateRule(
[]string{"anonymous", "oauth2_introspection"}, []string{"anonymous", "oauth2_introspection"},
[]string{"allow", "deny"}, []string{"allow", "deny"},
[]string{"id_token"}, []string{"id_token"},
),
}
router := httprouter.New()
handler.SetRoutes(router)
Expand Down Expand Up @@ -76,8 +81,15 @@ func TestHandler(t *testing.T) {
PreserveHost: false,
},
}
invalidRule := swagger.Rule{
Id: "foo3",
}

t.Run("case=create a new rule", func(t *testing.T) {
_, response, err := client.CreateRule(invalidRule)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, response.StatusCode)

result, response, err := client.CreateRule(r1)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, response.StatusCode)
Expand Down
94 changes: 94 additions & 0 deletions rule/rule_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright © 2017-2018 Aeneas Rekkas <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Aeneas Rekkas <[email protected]>
* @copyright 2017-2018 Aeneas Rekkas <[email protected]>
* @license Apache-2.0
*/

package rule

import (
"fmt"

"github.com/asaskevich/govalidator"
"github.com/ory/go-convenience/stringslice"
"github.com/ory/oathkeeper/helper"
"github.com/pkg/errors"
)

func ValidateRule(
enabledAuthenticators []string, availableAuthenticators []string,
enabledAuthorizers []string, availableAuthorizers []string,
enabledCredentialsIssuers []string, availableCredentialsIssuers []string,
) func(r *Rule) error {
methods := []string{"GET", "POST", "PUT", "HEAD", "DELETE", "PATCH", "OPTIONS", "TRACE", "CONNECT"}

return func(r *Rule) error {
if !govalidator.IsURL(r.Match.URL) {
return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Value \"%s\" from match.url field is not a valid url.", r.Match.URL)))
}

for _, m := range r.Match.Methods {
if !stringslice.Has(methods, m) {
return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Value \"%s\" from match.methods is not a valid HTTP method, valid methods are: %v", m, methods)))
}
}

if !govalidator.IsURL(r.Upstream.URL) {
return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Value \"%s\" from upstream.url field is not a valid url.", r.Upstream.URL)))
}

if len(r.Authenticators) == 0 {
return errors.WithStack(helper.ErrBadRequest.WithReason("At least one authenticator must be set."))
}

for _, a := range r.Authenticators {
if !stringslice.Has(enabledAuthenticators, a.Handler) {
if stringslice.Has(availableAuthenticators, a.Handler) {
return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Authenticator \"%s\" is valid but has not enabled by the server's configuration, enabled authorizers are: %v", a.Handler, enabledAuthenticators)))
}

return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Authenticator \"%s\" is unknown, enabled authenticators are: %v", a.Handler, enabledAuthenticators)))
}
}

if r.Authorizer.Handler == "" {
return errors.WithStack(helper.ErrBadRequest.WithReason("Value authorizer.handler can not be empty."))
}

if !stringslice.Has(enabledAuthorizers, r.Authorizer.Handler) {
if stringslice.Has(availableAuthorizers, r.Authorizer.Handler) {
return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Authorizer \"%s\" is valid but has not enabled by the server's configuration, enabled authorizers are: %v", r.Authorizer.Handler, enabledAuthorizers)))
}

return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Authorizer \"%s\" is unknown, enabled authorizers are: %v", r.Authorizer.Handler, enabledAuthorizers)))
}

if r.CredentialsIssuer.Handler == "" {
return errors.WithStack(helper.ErrBadRequest.WithReason("Value credentials_issuer.handler can not be empty."))
}

if !stringslice.Has(enabledCredentialsIssuers, r.CredentialsIssuer.Handler) {
if stringslice.Has(availableCredentialsIssuers, r.CredentialsIssuer.Handler) {
return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Credentials issuer \"%s\" is valid but has not enabled by the server's configuration, enabled credentials issuers are: %v", r.CredentialsIssuer.Handler, enabledCredentialsIssuers)))
}

return errors.WithStack(helper.ErrBadRequest.WithReason(fmt.Sprintf("Credentials issuer \"%s\" is unknown, enabled credentials issuers are: %v", r.CredentialsIssuer.Handler, enabledCredentialsIssuers)))
}

return nil
}
}
Loading

0 comments on commit f450697

Please sign in to comment.