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 remote_opa authorizer #462

Closed
wants to merge 7 commits into from
Closed

Add remote_opa authorizer #462

wants to merge 7 commits into from

Conversation

ryanph
Copy link

@ryanph ryanph commented Jun 5, 2020

Related issue

Issue #98 requests an OPA authoriser

Proposed changes

Implement an OPA authorizer that queries an OPA server via its REST API for authorization. OPA can also be used as a Go library directly within applications and as this authorizer interfaces with an OPA server using the REST API it is called 'remote_opa' rather than just 'opa'.

The existing generic http authorizers (remote and remote_json) rely on the remote service responding with a 200 or 403 response code to authorize requests. When using the OPA REST API to query a policy document with input (e.g. the details of a HTTP request) the response code is always 200 and it is the response body that contains the result of the policy. As such the existing authorizers are not suitable.

A quick way to test / demonstrate this authorizer:

Start an OPA instance:
docker run -p 8181:8181 openpolicyagent/opa run --server

Upload a basic policy:

cat <<EOF > policy.rego
package example.authz

import input

default allow = false

allow = true {
    input.method = "GET"
    input.path = ["profiles"]
}

allow = true {
    input.method = "POST"
    input.path = ["profiles", username ]
    input.user == username
}
EOF

curl -X PUT --data-binary @example/policy.rego localhost:8181/v1/policies/example

In this example policy anyone can GET /profiles, and users can POST to their own profile under /profiles/username (or in this example with the anonymous authenticator anyone can POST to /profiles/anonymous).

Configure the remote_opa authorizer to use this policy:

cat <<EOF > oathkeeper.yml
serve:
  proxy:
    port: 8000

authenticators:
  noop:
    enabled: true
  anonymous:
    enabled: true

authorizers:
  remote_opa:
    enabled: true
    config:
      remote: http://127.0.0.1:8181/v1/data/example/authz 
      payload: "{\"input\":{\"user\":\"{{ .Subject }}\",\"path\":{{ .PathArray }},\"method\":\"{{ .MatchContext.Method }}\"}}"
    
mutators:
  noop:
    enabled: true

access_rules:
  repositories:
    - "file://./rules.json"
EOF
cat <<EOF > rules.json
    [{
    "id": "rule-1",
    "upstream": {
        "url": "http://localhost:8080"
    },
    "match": {
        "url": "http://<[^\/]+>/<.*>",
        "methods": ["GET","POST"]
    },
    "authenticators": [
        {
        "handler": "anonymous"
        },
        {
        "handler": "noop"
        }
    ],
    "authorizer": {
        "handler": "remote_opa"
    },
    "mutators": [
        {
        "handler": "noop"
        }
    ]
    }]
EOF

The intention of this authorizer is to be a drop in replacement to the python example in the OPA HTTP APIs documentation (https://www.openpolicyagent.org/docs/latest/http-api-authorization/). Rather than allow the policy input sent to the OPA REST API to be templated it is opinionated to use the format from the OPA documentation. An example policy input generated by the module looks like this:

{
  "input": {
    "user": "Subject",
    "path": ["path1", "path2", "path3"],
    "method": "GET"
  }
}

An example response from the OPA REST API looks like this:

{
  "result": {
    "allow": true
  }
}

Checklist

  • [ X ] I have read the contributing guidelines.
  • [ X ] I have read the security policy.
  • [ X ] I confirm that this pull request does not address a security
    vulnerability. If this pull request addresses a security. vulnerability, I
    confirm that I got green light (please contact
    [email protected]) from the maintainers to push
    the changes.
  • [ X ] I have added tests that prove my fix is effective or that my feature
    works.
  • [ X ] I have added or changed the documentation

Further comments

This is the first Go project I've contributed to, more than happy for address feedback anyone has on any aspect of the code.

@CLAassistant
Copy link

CLAassistant commented Jun 5, 2020

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ aeneasr
❌ ryanph


ryanph seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Member

@aeneasr aeneasr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I've added a couple of comments :)

docs/docs/pipeline/authz.md Outdated Show resolved Hide resolved
docs/docs/pipeline/authz.md Outdated Show resolved Hide resolved
}
],
"authorizer": {
"handler": "remote_json",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"handler": "remote_json",
"handler": "remote_opa",


// OPA Policy Input
type OPAInput struct {
User string `json:"user"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This payload is bound to your model of access control and is not generalizable for other use cases. Instead, this should be configurable (see e.g. remote_json for an implementation). You might also want to wait for JsonNet support (#441) for this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payload for the OPA server needs the Method of the request, as well as the path of the request as an array. I believe I've added the Method correctly into the MatchContext structure, however I'm having a stab at the best way to add the Path "array" into the template.

Given it's specific to this module I don't think it makes sense to put it back in the AuthenticationSession struct but happy to take your guidance on that.

Copy link
Member

@aeneasr aeneasr Jun 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payload for the OPA server needs the Method of the request, as well as the path of the request as an array. I believe I've added the Method correctly into the MatchContext structure, however I'm having a stab at the best way to add the Path "array" into the template.

I don't think that is correct. You define the input model using Rego, so it depends on your Rego Policies what input you expect. You're probably referring to this example but as you can see from the Rego policy, the input parameters are defined by the policy itself, not by OPA. If you use a different Rego policy (one using e.g. input.foo_bar_baz) you would expect {"foo_bar_baz": "..."} in the JSON payload.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, the intention though was for it to be able to work with the OPA tutorial you linked "out of the box" and I've also seen this "path split into an array" in use by other developers I'm working with.

Any suggestions on how this could be achieved any other way using text/template, or do you have any objections to this approach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best approach would be using JsonNet (see #441) but I feat that needs quite some work on my end beforehand.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there's no way to implement this in an acceptable way without JsonNet so I'll close off this PR and look at it again at some point in the future

}

// Authorize implements the Authorizer interface.
func (a *AuthorizerRemoteOPA) Authorize(_ *http.Request, session *authn.AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a test :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've basically copied the remote_json test, still reading up on how Viper works and how to run the tests locally to implement some more specific tests for this authorizer.

.schemas/authorizers.remote_opa.schema.json Outdated Show resolved Hide resolved
@@ -0,0 +1,6 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.schemas/config.schema.json Outdated Show resolved Hide resolved
@ryanph ryanph closed this Jun 23, 2020
@ryanph ryanph deleted the remote_opa branch June 23, 2020 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants