Skip to content

Commit

Permalink
feat: add new remote authorizer that uses request body and headers (#416
Browse files Browse the repository at this point in the history
)

This pull request implements a new authorizer that sends the original request body as body to the remote endpoint. This allows the remote endpoint to take the body into account in its decision.

The current remote_json authorizer does not have the ability to send
the request body of the request to authorize. This means this cannot
be taken into account while checking permissions.

Providing the request body as part of the JSON payload won't always
work as JSON cannot handle binary data.
  • Loading branch information
Marlinc authored Apr 24, 2020
1 parent ad182f4 commit 3a20637
Show file tree
Hide file tree
Showing 10 changed files with 499 additions and 1 deletion.
58 changes: 58 additions & 0 deletions .schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,32 @@
],
"additionalProperties": false
},
"configAuthorizersRemote": {
"type": "object",
"title": "Remote Configuration",
"description": "This section is optional when the authorizer is disabled.",
"properties": {
"remote": {
"title": "Remote Authorizer URL",
"type": "string",
"format": "uri",
"description": "The URL of the remote authorizer. The remote authorizer is expected to return either 200 OK or 403 Forbidden to allow/deny access.\n\n>If this authorizer is enabled, this value is required.",
"examples": [
"https://host/path"
]
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"remote"
],
"additionalProperties": false
},
"configAuthorizersRemoteJSON": {
"type": "object",
"title": "Remote JSON Configuration",
Expand Down Expand Up @@ -1402,6 +1428,38 @@
}
]
},
"remote": {
"title": "Remote",
"description": "The [`remote` authorizer](https://www.ory.sh/oathkeeper/docs/pipeline/authz#remote).",
"type": "object",
"properties": {
"enabled": {
"$ref": "#/definitions/handlerSwitch"
}
},
"oneOf": [
{
"properties": {
"enabled": {
"const": true
},
"config": {
"$ref": "#/definitions/configAuthorizersRemote"
}
},
"required": [
"config"
]
},
{
"properties": {
"enabled": {
"const": false
}
}
}
]
},
"remote_json": {
"title": "Remote JSON",
"description": "The [`remote_json` authorizer](https://www.ory.sh/oathkeeper/docs/pipeline/authz#remote_json).",
Expand Down
5 changes: 5 additions & 0 deletions .schema/pipeline/authorizers.remote.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$id": "/.schema/authorizers.remote.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "/.schema/config.schema.json#/definitions/configAuthorizersRemote"
}
81 changes: 81 additions & 0 deletions docs/docs/pipeline/authz.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,87 @@ $ cat ./rules.json
}]
```

## `remote`

This authorizer performs authorization using a remote authorizer. The authorizer
makes a HTTP POST request to a remote endpoint with the original body request as
body. If the endpoint returns a "200 OK" response code, the access is allowed,
if it returns a "403 Forbidden" response code, the access is denied.

### Configuration

- `remote` (string, required) - The remote authorizer's URL. The remote
authorizer is expected to return either "200 OK" or "403 Forbidden" to
allow/deny access.
- `headers` (map of strings, optional) - The HTTP headers sent to the remote
authorizer. The values will be parsed by the Go
[`text/template`](https://golang.org/pkg/text/template/) package and applied
to an
[`AuthenticationSession`](https://github.com/ory/oathkeeper/blob/master/pipeline/authn/authenticator.go#L40)
object. See [Session](index.md#session) for more details.

#### Example

```yaml
# Global configuration file oathkeeper.yml
authorizers:
remote:
# Set enabled to "true" to enable the authenticator, and "false" to disable the authenticator. Defaults to "false".
enabled: true

config:
remote: http://my-remote-authorizer/authorize
headers:
X-Subject: "{{ print .Subject }}"
```
```yaml
# Some Access Rule: access-rule-1.yaml
id: access-rule-1
# match: ...
# upstream: ...
authorizers:
- handler: remote
config:
remote: http://my-remote-authorizer/authorize
headers:
X-Subject: "{{ print .Subject }}"
```
### Access Rule Example
```shell
{
"id": "some-id",
"upstream": {
"url": "http://my-backend-service"
},
"match": {
"url": "http://my-app/api/<.*>",
"methods": ["GET"]
},
"authenticators": [
{
"handler": "anonymous"
}
],
"authorizer": {
"handler": "remote",
"config": {
"remote": "http://my-remote-authorizer/authorize",
"headers": {
"X-Subject": "{{ print .Subject }}"
}
}
}
"mutators": [
{
"handler": "noop"
}
]
}
```

## `remote_json`

This authorizer performs authorization using a remote authorizer. The authorizer
Expand Down
2 changes: 2 additions & 0 deletions driver/configuration/provider_viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const (

ViperKeyAuthorizerKetoEngineACPORYIsEnabled = "authorizers.keto_engine_acp_ory.enabled"

ViperKeyAuthorizerRemoteIsEnabled = "authorizers.remote.enabled"

ViperKeyAuthorizerRemoteJSONIsEnabled = "authorizers.remote_json.enabled"
)

Expand Down
1 change: 1 addition & 0 deletions driver/registry_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ func (r *RegistryMemory) prepareAuthz() {
authz.NewAuthorizerAllow(r.c),
authz.NewAuthorizerDeny(r.c),
authz.NewAuthorizerKetoEngineACPORY(r.c),
authz.NewAuthorizerRemote(r.c),
authz.NewAuthorizerRemoteJSON(r.c),
}

Expand Down
3 changes: 2 additions & 1 deletion driver/registry_memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func TestRegistryMemoryAvailablePipelineAuthorizers(t *testing.T) {
r := NewRegistryMemory()
got := r.AvailablePipelineAuthorizers()
assert.ElementsMatch(t, got, []string{"allow", "deny", "keto_engine_acp_ory", "remote_json"})
assert.ElementsMatch(t, got, []string{"allow", "deny", "keto_engine_acp_ory", "remote", "remote_json"})
}

func TestRegistryMemoryPipelineAuthorizer(t *testing.T) {
Expand All @@ -20,6 +20,7 @@ func TestRegistryMemoryPipelineAuthorizer(t *testing.T) {
{id: "allow"},
{id: "deny"},
{id: "keto_engine_acp_ory"},
{id: "remote"},
{id: "remote_json"},
{id: "unregistered", wantErr: true},
}
Expand Down
9 changes: 9 additions & 0 deletions internal/config/.oathkeeper.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ authorizers:
required_action: unknown
required_resource: unknown

# Configures the remote authorizer
remote:
# Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false.
enabled: true

config:
remote: https://host/path
headers: {}

# Configures the remote_json authorizer
remote_json:
# Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false.
Expand Down
129 changes: 129 additions & 0 deletions pipeline/authz/remote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package authz

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"text/template"

"github.com/pkg/errors"

"github.com/ory/x/httpx"

"github.com/ory/oathkeeper/driver/configuration"
"github.com/ory/oathkeeper/helper"
"github.com/ory/oathkeeper/pipeline"
"github.com/ory/oathkeeper/pipeline/authn"
"github.com/ory/oathkeeper/x"
)

// AuthorizerRemoteConfiguration represents a configuration for the remote authorizer.
type AuthorizerRemoteConfiguration struct {
Remote string `json:"remote"`
Headers map[string]string `json:"headers"`
}

// AuthorizerRemote implements the Authorizer interface.
type AuthorizerRemote struct {
c configuration.Provider

client *http.Client
t *template.Template
}

// NewAuthorizerRemote creates a new AuthorizerRemote.
func NewAuthorizerRemote(c configuration.Provider) *AuthorizerRemote {
return &AuthorizerRemote{
c: c,
client: httpx.NewResilientClientLatencyToleranceSmall(nil),
t: x.NewTemplate("remote"),
}
}

// GetID implements the Authorizer interface.
func (a *AuthorizerRemote) GetID() string {
return "remote"
}

// Authorize implements the Authorizer interface.
func (a *AuthorizerRemote) Authorize(r *http.Request, session *authn.AuthenticationSession, config json.RawMessage, rl pipeline.Rule) error {
c, err := a.Config(config)
if err != nil {
return err
}

var body bytes.Buffer
err = pipeRequestBody(r, &body)
if err != nil {
return errors.Wrapf(err, `could not pipe request body in rule "%s"`, rl.GetID())
}

req, err := http.NewRequest("POST", c.Remote, ioutil.NopCloser(&body))
if err != nil {
return errors.WithStack(err)
}
req.Header.Add("Content-Type", r.Header.Get("Content-Type"))

for hdr, templateString := range c.Headers {
var tmpl *template.Template
var err error

templateId := fmt.Sprintf("%s:%s", rl.GetID(), hdr)
tmpl = a.t.Lookup(templateId)
if tmpl == nil {
tmpl, err = a.t.New(templateId).Parse(templateString)
if err != nil {
return errors.Wrapf(err, `error parsing headers template "%s" in rule "%s"`, templateString, rl.GetID())
}
}

headerValue := bytes.Buffer{}
err = tmpl.Execute(&headerValue, session)
if err != nil {
return errors.Wrapf(err, `error executing headers template "%s" in rule "%s"`, templateString, rl.GetID())
}
// Don't send empty headers
if headerValue.String() == "" {
continue
}

req.Header.Set(hdr, headerValue.String())
}

res, err := a.client.Do(req)
if err != nil {
return errors.WithStack(err)
}
defer res.Body.Close()

if res.StatusCode == http.StatusForbidden {
return errors.WithStack(helper.ErrForbidden)
} else if res.StatusCode != http.StatusOK {
return errors.Errorf("expected status code %d but got %d", http.StatusOK, res.StatusCode)
}

return nil
}

// Validate implements the Authorizer interface.
func (a *AuthorizerRemote) Validate(config json.RawMessage) error {
if !a.c.AuthorizerIsEnabled(a.GetID()) {
return NewErrAuthorizerNotEnabled(a)
}

_, err := a.Config(config)
return err
}

// Config merges config and the authorizer's configuration and validates the
// resulting configuration. It reports an error if the configuration is invalid.
func (a *AuthorizerRemote) Config(config json.RawMessage) (*AuthorizerRemoteConfiguration, error) {
var c AuthorizerRemoteConfiguration
if err := a.c.AuthorizerConfig(a.GetID(), config, &c); err != nil {
return nil, NewErrAuthorizerMisconfigured(a, err)
}

return &c, nil
}
Loading

0 comments on commit 3a20637

Please sign in to comment.