Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new remote authorizer that uses request body and headers #416

Merged
merged 1 commit into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Marlinc marked this conversation as resolved.
Show resolved Hide resolved
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