Skip to content

Commit

Permalink
feat(authz): Add remote_json authorizer (#389)
Browse files Browse the repository at this point in the history
This patch adds the `remote_json` authorizer as documented here: ory/docs@07a2297#diff-c400219db6c7e4b6abab71839d9d294eR272

Closes #201
  • Loading branch information
kaorimatz authored Mar 29, 2020
1 parent b817037 commit 45b9f8b
Show file tree
Hide file tree
Showing 10 changed files with 480 additions and 0 deletions.
31 changes: 31 additions & 0 deletions .schemas/authorizers.remote_json.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$id": "https://raw.githubusercontent.com/ory/oathkeeper/master/.schemas/authorizers.remote_json.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Remote JSON 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"
]
},
"payload": {
"title": "JSON Payload",
"type": "string",
"description": "The JSON payload of the request sent to the remote authorizer. The string will be parsed by the Go text/template package and applied to an AuthenticationSession object.\n\n>If this authorizer is enabled, this value is required.",
"examples": [
"{\"subject\":\"{{ .Subject }}\"}"
]
}
},
"required": [
"remote",
"payload"
],
"additionalProperties": false
}
61 changes: 61 additions & 0 deletions .schemas/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,35 @@
],
"additionalProperties": false
},
"configAuthorizersRemoteJSON": {
"type": "object",
"title": "Remote JSON 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"
]
},
"payload": {
"title": "JSON Payload",
"type": "string",
"description": "The JSON payload of the request sent to the remote authorizer. The string will be parsed by the Go text/template package and applied to an AuthenticationSession object.\n\n>If this authorizer is enabled, this value is required.",
"examples": [
"{\"subject\":\"{{ .Subject }}\"}"
]
}
},
"required": [
"remote",
"payload"
],
"additionalProperties": false
},
"configMutatorsCookie": {
"type": "object",
"title": "Cookie Mutator Configuration",
Expand Down Expand Up @@ -1372,6 +1401,38 @@
}
}
]
},
"remote_json": {
"title": "Remote JSON",
"description": "The [`remote_json` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#remote_json).",
"type": "object",
"properties": {
"enabled": {
"$ref": "#/definitions/handlerSwitch"
}
},
"oneOf": [
{
"properties": {
"enabled": {
"const": true
},
"config": {
"$ref": "#/definitions/configAuthorizersRemoteJSON"
}
},
"required": [
"config"
]
},
{
"properties": {
"enabled": {
"const": false
}
}
}
]
}
}
},
Expand Down
5 changes: 5 additions & 0 deletions .schemas/pipeline/authorizers.remote_json.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$id": "https://raw.githubusercontent.com/ory/oathkeeper/v0.34.0-beta.1/.schemas/authorizers.remote_json.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "https://raw.githubusercontent.com/ory/oathkeeper/v0.34.0-beta.1/.schemas/config.schema.json#/definitions/configAuthorizersRemoteJSON"
}
9 changes: 9 additions & 0 deletions docs/.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_json authorizer
remote_json:
# 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
payload: "{}"

# All mutators can be configured under this configuration key
mutators:
header:
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 @@ -56,6 +56,8 @@ const (
ViperKeyAuthorizerDenyIsEnabled = "authorizers.deny.enabled"

ViperKeyAuthorizerKetoEngineACPORYIsEnabled = "authorizers.keto_engine_acp_ory.enabled"

ViperKeyAuthorizerRemoteJSONIsEnabled = "authorizers.remote_json.enabled"
)

// Mutators
Expand Down
12 changes: 12 additions & 0 deletions driver/configuration/provider_viper_public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,18 @@ func TestViperProvider(t *testing.T) {

assert.EqualValues(t, "http://my-keto/", config.BaseURL)
})

t.Run("authorizer=remote_json", func(t *testing.T) {
a := authz.NewAuthorizerRemoteJSON(p)
assert.True(t, p.AuthorizerIsEnabled(a.GetID()))
require.NoError(t, a.Validate(nil))

config, err := a.Config(nil)
require.NoError(t, err)

assert.EqualValues(t, "https://host/path", config.Remote)
assert.EqualValues(t, "{}", config.Payload)
})
})

t.Run("group=mutators", func(t *testing.T) {
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.NewAuthorizerRemoteJSON(r.c),
}

r.authorizers = map[string]authz.Authorizer{}
Expand Down
39 changes: 39 additions & 0 deletions driver/registry_memory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package driver

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRegistryMemoryAvailablePipelineAuthorizers(t *testing.T) {
r := NewRegistryMemory()
got := r.AvailablePipelineAuthorizers()
assert.ElementsMatch(t, got, []string{"allow", "deny", "keto_engine_acp_ory", "remote_json"})
}

func TestRegistryMemoryPipelineAuthorizer(t *testing.T) {
tests := []struct {
id string
wantErr bool
}{
{id: "allow"},
{id: "deny"},
{id: "keto_engine_acp_ory"},
{id: "remote_json"},
{id: "unregistered", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.id, func(t *testing.T) {
r := NewRegistryMemory()
a, err := r.PipelineAuthorizer(tt.id)
if (err != nil) != tt.wantErr {
t.Errorf("PipelineAuthorizer() error = %v, wantErr %v", err, tt.wantErr)
return
}
if a != nil && a.GetID() != tt.id {
t.Errorf("PipelineAuthorizer() got = %v, want %v", a.GetID(), tt.id)
}
})
}
}
121 changes: 121 additions & 0 deletions pipeline/authz/remote_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package authz

import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"text/template"

"github.com/ory/x/httpx"
"github.com/pkg/errors"

"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"
)

// AuthorizerRemoteJSONConfiguration represents a configuration for the remote_json authorizer.
type AuthorizerRemoteJSONConfiguration struct {
Remote string `json:"remote"`
Payload string `json:"payload"`
}

// PayloadTemplateID returns a string with which to associate the payload template.
func (c *AuthorizerRemoteJSONConfiguration) PayloadTemplateID() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(c.Payload)))
}

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

client *http.Client
t *template.Template
}

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

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

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

templateID := c.PayloadTemplateID()
t := a.t.Lookup(templateID)
if t == nil {
var err error
t, err = a.t.New(templateID).Parse(c.Payload)
if err != nil {
return errors.WithStack(err)
}
}

var body bytes.Buffer
if err := t.Execute(&body, session); err != nil {
return errors.WithStack(err)
}

var j json.RawMessage
if err := json.Unmarshal(body.Bytes(), &j); err != nil {
return errors.Wrap(err, "payload is not a JSON text")
}

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

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 *AuthorizerRemoteJSON) 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 *AuthorizerRemoteJSON) Config(config json.RawMessage) (*AuthorizerRemoteJSONConfiguration, error) {
var c AuthorizerRemoteJSONConfiguration
if err := a.c.AuthorizerConfig(a.GetID(), config, &c); err != nil {
return nil, NewErrAuthorizerMisconfigured(a, err)
}

return &c, nil
}
Loading

0 comments on commit 45b9f8b

Please sign in to comment.