mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-30 10:56:28 +02:00
ppl: add support for http_path and http_method (#2813)
* ppl: add support for http_path and http_method * fix import ordering
This commit is contained in:
parent
54ec88fb93
commit
2d04106e6d
13 changed files with 257 additions and 18 deletions
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v3"
|
"github.com/go-jose/go-jose/v3"
|
||||||
"github.com/open-policy-agent/opa/rego"
|
"github.com/open-policy-agent/opa/rego"
|
||||||
|
@ -35,11 +36,23 @@ type Request struct {
|
||||||
// RequestHTTP is the HTTP field in the request.
|
// RequestHTTP is the HTTP field in the request.
|
||||||
type RequestHTTP struct {
|
type RequestHTTP struct {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
|
Path string `json:"path"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Headers map[string]string `json:"headers"`
|
Headers map[string]string `json:"headers"`
|
||||||
ClientCertificate string `json:"client_certificate"`
|
ClientCertificate string `json:"client_certificate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRequestHTTP creates a new RequestHTTP.
|
||||||
|
func NewRequestHTTP(method string, requestURL url.URL, headers map[string]string, rawClientCertificate string) RequestHTTP {
|
||||||
|
return RequestHTTP{
|
||||||
|
Method: method,
|
||||||
|
Path: requestURL.Path,
|
||||||
|
URL: requestURL.String(),
|
||||||
|
Headers: headers,
|
||||||
|
ClientCertificate: rawClientCertificate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RequestSession is the session field in the request.
|
// RequestSession is the session field in the request.
|
||||||
type RequestSession struct {
|
type RequestSession struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
"github.com/pomerium/pomerium/pkg/policy/criteria"
|
"github.com/pomerium/pomerium/pkg/policy/criteria"
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
"github.com/pomerium/pomerium/pkg/protoutil"
|
"github.com/pomerium/pomerium/pkg/protoutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -86,6 +87,36 @@ func TestEvaluator(t *testing.T) {
|
||||||
To: config.WeightedURLs{{URL: *mustParseURL("https://to9.example.com")}},
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to9.example.com")}},
|
||||||
AllowAnyAuthenticatedUser: true,
|
AllowAnyAuthenticatedUser: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to10.example.com")}},
|
||||||
|
Policy: &config.PPLPolicy{
|
||||||
|
Policy: &parser.Policy{
|
||||||
|
Rules: []parser.Rule{{
|
||||||
|
Action: parser.ActionAllow,
|
||||||
|
Or: []parser.Criterion{{
|
||||||
|
Name: "http_method", Data: parser.Object{
|
||||||
|
"is": parser.String("GET"),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
To: config.WeightedURLs{{URL: *mustParseURL("https://to11.example.com")}},
|
||||||
|
Policy: &config.PPLPolicy{
|
||||||
|
Policy: &parser.Policy{
|
||||||
|
Rules: []parser.Rule{{
|
||||||
|
Action: parser.ActionAllow,
|
||||||
|
Or: []parser.Criterion{{
|
||||||
|
Name: "http_path", Data: parser.Object{
|
||||||
|
"is": parser.String("/test"),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
options := []Option{
|
options := []Option{
|
||||||
WithAuthenticateURL("https://authn.example.com"),
|
WithAuthenticateURL("https://authn.example.com"),
|
||||||
|
@ -442,6 +473,32 @@ func TestEvaluator(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("http method", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{}, &Request{
|
||||||
|
Policy: &policies[9],
|
||||||
|
HTTP: NewRequestHTTP(
|
||||||
|
"GET",
|
||||||
|
*mustParseURL("https://from.example.com/"),
|
||||||
|
nil,
|
||||||
|
testValidCert,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow.Value)
|
||||||
|
})
|
||||||
|
t.Run("http path", func(t *testing.T) {
|
||||||
|
res, err := eval(t, options, []proto.Message{}, &Request{
|
||||||
|
Policy: &policies[10],
|
||||||
|
HTTP: NewRequestHTTP(
|
||||||
|
"POST",
|
||||||
|
*mustParseURL("https://from.example.com/test"),
|
||||||
|
nil,
|
||||||
|
testValidCert,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, res.Allow.Value)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustParseURL(str string) *url.URL {
|
func mustParseURL(str string) *url.URL {
|
||||||
|
|
|
@ -124,12 +124,12 @@ func (a *Authorize) getEvaluatorRequestFromCheckRequest(
|
||||||
) (*evaluator.Request, error) {
|
) (*evaluator.Request, error) {
|
||||||
requestURL := getCheckRequestURL(in)
|
requestURL := getCheckRequestURL(in)
|
||||||
req := &evaluator.Request{
|
req := &evaluator.Request{
|
||||||
HTTP: evaluator.RequestHTTP{
|
HTTP: evaluator.NewRequestHTTP(
|
||||||
Method: in.GetAttributes().GetRequest().GetHttp().GetMethod(),
|
in.GetAttributes().GetRequest().GetHttp().GetMethod(),
|
||||||
URL: requestURL.String(),
|
requestURL,
|
||||||
Headers: getCheckRequestHeaders(in),
|
getCheckRequestHeaders(in),
|
||||||
ClientCertificate: getPeerCertificate(in),
|
getPeerCertificate(in),
|
||||||
},
|
),
|
||||||
}
|
}
|
||||||
if sessionState != nil {
|
if sessionState != nil {
|
||||||
req.Session = evaluator.RequestSession{
|
req.Session = evaluator.RequestSession{
|
||||||
|
|
|
@ -90,15 +90,15 @@ func Test_getEvaluatorRequest(t *testing.T) {
|
||||||
Session: evaluator.RequestSession{
|
Session: evaluator.RequestSession{
|
||||||
ID: "SESSION_ID",
|
ID: "SESSION_ID",
|
||||||
},
|
},
|
||||||
HTTP: evaluator.RequestHTTP{
|
HTTP: evaluator.NewRequestHTTP(
|
||||||
Method: "GET",
|
"GET",
|
||||||
URL: "http://example.com/some/path?qs=1",
|
mustParseURL("http://example.com/some/path?qs=1"),
|
||||||
Headers: map[string]string{
|
map[string]string{
|
||||||
"Accept": "text/html",
|
"Accept": "text/html",
|
||||||
"X-Forwarded-Proto": "https",
|
"X-Forwarded-Proto": "https",
|
||||||
},
|
},
|
||||||
ClientCertificate: certPEM,
|
certPEM,
|
||||||
},
|
),
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, actual)
|
assert.Equal(t, expect, actual)
|
||||||
}
|
}
|
||||||
|
@ -296,15 +296,15 @@ func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) {
|
||||||
expect := &evaluator.Request{
|
expect := &evaluator.Request{
|
||||||
Policy: &a.currentOptions.Load().Policies[0],
|
Policy: &a.currentOptions.Load().Policies[0],
|
||||||
Session: evaluator.RequestSession{},
|
Session: evaluator.RequestSession{},
|
||||||
HTTP: evaluator.RequestHTTP{
|
HTTP: evaluator.NewRequestHTTP(
|
||||||
Method: "GET",
|
"GET",
|
||||||
URL: "http://example.com/some/path?qs=1",
|
mustParseURL("http://example.com/some/path?qs=1"),
|
||||||
Headers: map[string]string{
|
map[string]string{
|
||||||
"Accept": "text/html",
|
"Accept": "text/html",
|
||||||
"X-Forwarded-Proto": "https",
|
"X-Forwarded-Proto": "https",
|
||||||
},
|
},
|
||||||
ClientCertificate: certPEM,
|
certPEM,
|
||||||
},
|
),
|
||||||
}
|
}
|
||||||
assert.Equal(t, expect, actual)
|
assert.Equal(t, expect, actual)
|
||||||
}
|
}
|
||||||
|
@ -414,3 +414,11 @@ func TestAuthorize_Check(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustParseURL(rawURL string) url.URL {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return *u
|
||||||
|
}
|
||||||
|
|
|
@ -83,6 +83,8 @@ PPL supports many different criteria:
|
||||||
| `domain` | String Matcher | Returns true if the logged-in user's email address domain (the part after `@`) matches the given value. |
|
| `domain` | String Matcher | Returns true if the logged-in user's email address domain (the part after `@`) matches the given value. |
|
||||||
| `email` | String Matcher | Returns true if the logged-in user's email address matches the given value. |
|
| `email` | String Matcher | Returns true if the logged-in user's email address matches the given value. |
|
||||||
| `groups` | List Matcher | Returns true if the logged-in user is a member of the given group. |
|
| `groups` | List Matcher | Returns true if the logged-in user is a member of the given group. |
|
||||||
|
| `http_method` | String Matcher | Returns true if the HTTP method matches the given value. |
|
||||||
|
| `http_path` | String Matcher | Returns true if the HTTP path matches the given value. |
|
||||||
| `invalid_client_certificate` | Anything. Typically `true`. | Returns true if the incoming request has an invalid client certificate. A default `deny` rule using this criterion is added to all Pomerium policies when an mTLS [client certificate authority] is set. |
|
| `invalid_client_certificate` | Anything. Typically `true`. | Returns true if the incoming request has an invalid client certificate. A default `deny` rule using this criterion is added to all Pomerium policies when an mTLS [client certificate authority] is set. |
|
||||||
| `pomerium_routes` | Anything. Typically `true`. | Returns true if the incoming request is for the special `.pomerium` routes. A default `allow` rule using this criterion is added to all Pomerium policies. |
|
| `pomerium_routes` | Anything. Typically `true`. | Returns true if the incoming request is for the special `.pomerium` routes. A default `allow` rule using this criterion is added to all Pomerium policies. |
|
||||||
| `reject` | Anything. Typically `true`. | Always returns false. The opposite of `accept`. |
|
| `reject` | Anything. Typically `true`. | Always returns false. The opposite of `accept`. |
|
||||||
|
|
|
@ -142,6 +142,8 @@ settings:
|
||||||
- `domain`
|
- `domain`
|
||||||
- `email`
|
- `email`
|
||||||
- `groups`
|
- `groups`
|
||||||
|
- `http_method`
|
||||||
|
- `http_path`
|
||||||
- `reject`
|
- `reject`
|
||||||
- `time_of_day`
|
- `time_of_day`
|
||||||
- `user`
|
- `user`
|
||||||
|
|
|
@ -333,6 +333,8 @@ The available criteria types are:
|
||||||
- `domain`
|
- `domain`
|
||||||
- `email`
|
- `email`
|
||||||
- `groups`
|
- `groups`
|
||||||
|
- `http_method`
|
||||||
|
- `http_path`
|
||||||
- `reject`
|
- `reject`
|
||||||
- `time_of_day`
|
- `time_of_day`
|
||||||
- `user`
|
- `user`
|
||||||
|
|
|
@ -32,6 +32,7 @@ type (
|
||||||
}
|
}
|
||||||
InputHTTP struct {
|
InputHTTP struct {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
|
Path string `json:"path"`
|
||||||
Headers map[string][]string `json:"headers"`
|
Headers map[string][]string `json:"headers"`
|
||||||
}
|
}
|
||||||
InputSession struct {
|
InputSession struct {
|
||||||
|
|
43
pkg/policy/criteria/http_method.go
Normal file
43
pkg/policy/criteria/http_method.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpMethodCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (httpMethodCriterion) DataType() CriterionDataType {
|
||||||
|
return CriterionDataTypeStringMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (httpMethodCriterion) Name() string {
|
||||||
|
return "http_method"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c httpMethodCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
var body ast.Body
|
||||||
|
ref := ast.RefTerm(ast.VarTerm("input"), ast.VarTerm("http"), ast.VarTerm("method"))
|
||||||
|
err := matchString(&body, ref, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := NewCriterionRule(c.g, c.Name(),
|
||||||
|
ReasonHTTPMethodOK, ReasonHTTPMethodUnauthorized,
|
||||||
|
body)
|
||||||
|
|
||||||
|
return rule, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPMethod returns a Criterion which matches an HTTP method.
|
||||||
|
func HTTPMethod(generator *Generator) Criterion {
|
||||||
|
return httpMethodCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(HTTPMethod)
|
||||||
|
}
|
32
pkg/policy/criteria/http_method_test.go
Normal file
32
pkg/policy/criteria/http_method_test.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPMethod(t *testing.T) {
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- http_method:
|
||||||
|
is: GET
|
||||||
|
`, []dataBrokerRecord{}, Input{HTTP: InputHTTP{Method: "GET"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{true, A{ReasonHTTPMethodOK}, M{}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("unauthorized", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- http_method:
|
||||||
|
is: GET
|
||||||
|
`, []dataBrokerRecord{}, Input{HTTP: InputHTTP{Method: "POST"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonHTTPMethodUnauthorized}, M{}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
43
pkg/policy/criteria/http_path.go
Normal file
43
pkg/policy/criteria/http_path.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/pkg/policy/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpPathCriterion struct {
|
||||||
|
g *Generator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (httpPathCriterion) DataType() CriterionDataType {
|
||||||
|
return CriterionDataTypeStringMatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (httpPathCriterion) Name() string {
|
||||||
|
return "http_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c httpPathCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, []*ast.Rule, error) {
|
||||||
|
var body ast.Body
|
||||||
|
ref := ast.RefTerm(ast.VarTerm("input"), ast.VarTerm("http"), ast.VarTerm("path"))
|
||||||
|
err := matchString(&body, ref, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := NewCriterionRule(c.g, c.Name(),
|
||||||
|
ReasonHTTPPathOK, ReasonHTTPPathUnauthorized,
|
||||||
|
body)
|
||||||
|
|
||||||
|
return rule, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPPath returns a Criterion which matches an HTTP path.
|
||||||
|
func HTTPPath(generator *Generator) Criterion {
|
||||||
|
return httpPathCriterion{g: generator}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(HTTPPath)
|
||||||
|
}
|
32
pkg/policy/criteria/http_path_test.go
Normal file
32
pkg/policy/criteria/http_path_test.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package criteria
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPPath(t *testing.T) {
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- http_path:
|
||||||
|
is: /test
|
||||||
|
`, []dataBrokerRecord{}, Input{HTTP: InputHTTP{Path: "/test"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{true, A{ReasonHTTPPathOK}, M{}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
t.Run("unauthorized", func(t *testing.T) {
|
||||||
|
res, err := evaluate(t, `
|
||||||
|
allow:
|
||||||
|
and:
|
||||||
|
- http_path:
|
||||||
|
is: /test
|
||||||
|
`, []dataBrokerRecord{}, Input{HTTP: InputHTTP{Path: "/not-test"}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, A{false, A{ReasonHTTPPathUnauthorized}, M{}}, res["allow"])
|
||||||
|
require.Equal(t, A{false, A{}}, res["deny"])
|
||||||
|
})
|
||||||
|
}
|
|
@ -20,6 +20,10 @@ const (
|
||||||
ReasonEmailUnauthorized = "email-unauthorized"
|
ReasonEmailUnauthorized = "email-unauthorized"
|
||||||
ReasonGroupsOK = "groups-ok"
|
ReasonGroupsOK = "groups-ok"
|
||||||
ReasonGroupsUnauthorized = "groups-unauthorized"
|
ReasonGroupsUnauthorized = "groups-unauthorized"
|
||||||
|
ReasonHTTPMethodOK = "http-method-ok"
|
||||||
|
ReasonHTTPMethodUnauthorized = "http-method-unauthorized"
|
||||||
|
ReasonHTTPPathOK = "http-path-ok"
|
||||||
|
ReasonHTTPPathUnauthorized = "http-path-unauthorized"
|
||||||
ReasonInvalidClientCertificate = "invalid-client-certificate"
|
ReasonInvalidClientCertificate = "invalid-client-certificate"
|
||||||
ReasonNonCORSRequest = "non-cors-request"
|
ReasonNonCORSRequest = "non-cors-request"
|
||||||
ReasonNonPomeriumRoute = "non-pomerium-route"
|
ReasonNonPomeriumRoute = "non-pomerium-route"
|
||||||
|
|
Loading…
Add table
Reference in a new issue