mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 18:36:30 +02:00
* ppl: refactor authorize to evaluate PPL * remove opa test step * add log statement * simplify assignment * deny with forbidden if logged in * add safeEval function * create evaluator-specific config and options * embed the headers rego file directly
229 lines
6 KiB
Go
229 lines
6 KiB
Go
// Package evaluator contains rego evaluators for evaluating authorize policy.
|
|
package evaluator
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/open-policy-agent/opa/rego"
|
|
"gopkg.in/square/go-jose.v2"
|
|
|
|
"github.com/pomerium/pomerium/config"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/urlutil"
|
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
)
|
|
|
|
// notFoundOutput is what's returned if a route isn't found for a policy.
|
|
var notFoundOutput = &Result{
|
|
Allow: false,
|
|
Deny: &Denial{
|
|
Status: http.StatusNotFound,
|
|
Message: "route not found",
|
|
},
|
|
Headers: make(http.Header),
|
|
}
|
|
|
|
// Request contains the inputs needed for evaluation.
|
|
type Request struct {
|
|
Policy *config.Policy
|
|
HTTP RequestHTTP
|
|
Session RequestSession
|
|
}
|
|
|
|
// RequestHTTP is the HTTP field in the request.
|
|
type RequestHTTP struct {
|
|
Method string `json:"method"`
|
|
URL string `json:"url"`
|
|
Headers map[string]string `json:"headers"`
|
|
ClientCertificate string `json:"client_certificate"`
|
|
}
|
|
|
|
// RequestSession is the session field in the request.
|
|
type RequestSession struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// Result is the result of evaluation.
|
|
type Result struct {
|
|
Allow bool
|
|
Deny *Denial
|
|
Headers http.Header
|
|
|
|
DataBrokerServerVersion, DataBrokerRecordVersion uint64
|
|
}
|
|
|
|
// An Evaluator evaluates policies.
|
|
type Evaluator struct {
|
|
store *Store
|
|
policyEvaluators map[uint64]*PolicyEvaluator
|
|
headersEvaluators *HeadersEvaluator
|
|
clientCA []byte
|
|
}
|
|
|
|
// New creates a new Evaluator.
|
|
func New(ctx context.Context, store *Store, options ...Option) (*Evaluator, error) {
|
|
e := &Evaluator{store: store}
|
|
|
|
cfg := getConfig(options...)
|
|
|
|
err := e.updateStore(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
e.headersEvaluators, err = NewHeadersEvaluator(ctx, store)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
e.policyEvaluators = make(map[uint64]*PolicyEvaluator)
|
|
for _, configPolicy := range cfg.policies {
|
|
id, err := configPolicy.RouteID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authorize: error computing policy route id: %w", err)
|
|
}
|
|
policyEvaluator, err := NewPolicyEvaluator(ctx, store, &configPolicy) //nolint
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.policyEvaluators[id] = policyEvaluator
|
|
}
|
|
|
|
e.clientCA = cfg.clientCA
|
|
|
|
return e, nil
|
|
}
|
|
|
|
// Evaluate evaluates the rego for the given policy and generates the identity headers.
|
|
func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error) {
|
|
if req.Policy == nil {
|
|
return notFoundOutput, nil
|
|
}
|
|
|
|
id, err := req.Policy.RouteID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authorize: error computing policy route id: %w", err)
|
|
}
|
|
|
|
policyEvaluator, ok := e.policyEvaluators[id]
|
|
if !ok {
|
|
return notFoundOutput, nil
|
|
}
|
|
|
|
clientCA, err := e.getClientCA(req.Policy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isValidClientCertificate, err := isValidClientCertificate(clientCA, req.HTTP.ClientCertificate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authorize: error validating client certificate: %w", err)
|
|
}
|
|
|
|
policyOutput, err := policyEvaluator.Evaluate(ctx, &PolicyRequest{
|
|
HTTP: req.HTTP,
|
|
Session: req.Session,
|
|
IsValidClientCertificate: isValidClientCertificate,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
headersReq := NewHeadersRequestFromPolicy(req.Policy)
|
|
headersReq.Session = req.Session
|
|
headersOutput, err := e.headersEvaluators.Evaluate(ctx, headersReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &Result{
|
|
Allow: policyOutput.Allow,
|
|
Deny: policyOutput.Deny,
|
|
Headers: headersOutput.Headers,
|
|
}
|
|
res.DataBrokerServerVersion, res.DataBrokerRecordVersion = e.store.GetDataBrokerVersions()
|
|
return res, nil
|
|
}
|
|
|
|
func (e *Evaluator) getClientCA(policy *config.Policy) (string, error) {
|
|
if policy != nil && policy.TLSDownstreamClientCA != "" {
|
|
bs, err := base64.StdEncoding.DecodeString(policy.TLSDownstreamClientCA)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(bs), nil
|
|
}
|
|
|
|
return string(e.clientCA), nil
|
|
}
|
|
|
|
func (e *Evaluator) updateStore(cfg *evaluatorConfig) error {
|
|
jwk, err := getJWK(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("authorize: couldn't create signer: %w", err)
|
|
}
|
|
|
|
authenticateURL, err := urlutil.ParseAndValidateURL(cfg.authenticateURL)
|
|
if err != nil {
|
|
return fmt.Errorf("authorize: invalid authenticate URL: %w", err)
|
|
}
|
|
|
|
e.store.UpdateIssuer(authenticateURL.Host)
|
|
e.store.UpdateGoogleCloudServerlessAuthenticationServiceAccount(
|
|
cfg.googleCloudServerlessAuthenticationServiceAccount,
|
|
)
|
|
e.store.UpdateJWTClaimHeaders(cfg.jwtClaimsHeaders)
|
|
e.store.UpdateRoutePolicies(cfg.policies)
|
|
e.store.UpdateSigningKey(jwk)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getJWK(cfg *evaluatorConfig) (*jose.JSONWebKey, error) {
|
|
var decodedCert []byte
|
|
// if we don't have a signing key, generate one
|
|
if cfg.signingKey == "" {
|
|
key, err := cryptutil.NewSigningKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
|
}
|
|
decodedCert, err = cryptutil.EncodePrivateKey(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bad signing key: %w", err)
|
|
}
|
|
} else {
|
|
var err error
|
|
decodedCert, err = base64.StdEncoding.DecodeString(cfg.signingKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bad signing key: %w", err)
|
|
}
|
|
}
|
|
signingKeyAlgorithm := cfg.signingKeyAlgorithm
|
|
if signingKeyAlgorithm == "" {
|
|
signingKeyAlgorithm = string(jose.ES256)
|
|
}
|
|
|
|
jwk, err := cryptutil.PrivateJWKFromBytes(decodedCert, jose.SignatureAlgorithm(signingKeyAlgorithm))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate signing key: %w", err)
|
|
}
|
|
log.Info(context.TODO()).Str("Algorithm", jwk.Algorithm).
|
|
Str("KeyID", jwk.KeyID).
|
|
Interface("Public Key", jwk.Public()).
|
|
Msg("authorize: signing key")
|
|
|
|
return jwk, nil
|
|
}
|
|
|
|
func safeEval(ctx context.Context, q rego.PreparedEvalQuery, options ...rego.EvalOption) (resultSet rego.ResultSet, err error) {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("%v", e)
|
|
}
|
|
}()
|
|
resultSet, err = q.Eval(ctx, options...)
|
|
return resultSet, err
|
|
}
|