mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 18:36:30 +02:00
219 lines
5.3 KiB
Go
219 lines
5.3 KiB
Go
package evaluator
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/open-policy-agent/opa/rego"
|
|
octrace "go.opencensus.io/trace"
|
|
|
|
"github.com/pomerium/pomerium/config"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
"github.com/pomerium/pomerium/pkg/policy"
|
|
)
|
|
|
|
// PolicyRequest is the input to policy evaluation.
|
|
type PolicyRequest struct {
|
|
HTTP RequestHTTP `json:"http"`
|
|
Session RequestSession `json:"session"`
|
|
IsValidClientCertificate bool `json:"is_valid_client_certificate"`
|
|
}
|
|
|
|
// PolicyResponse is the result of evaluating a policy.
|
|
type PolicyResponse struct {
|
|
Allow bool
|
|
Deny *Denial
|
|
}
|
|
|
|
// Merge merges another PolicyResponse into this PolicyResponse. Access is allowed if either is allowed. Access is denied if
|
|
// either is denied. (and denials take precedence)
|
|
func (res *PolicyResponse) Merge(other *PolicyResponse) *PolicyResponse {
|
|
merged := &PolicyResponse{
|
|
Allow: res.Allow || other.Allow,
|
|
Deny: res.Deny,
|
|
}
|
|
if other.Deny != nil {
|
|
merged.Deny = other.Deny
|
|
}
|
|
return merged
|
|
}
|
|
|
|
// A Denial indicates the request should be denied (even if otherwise allowed).
|
|
type Denial struct {
|
|
Status int
|
|
Message string
|
|
}
|
|
|
|
type policyQuery struct {
|
|
rego.PreparedEvalQuery
|
|
checksum string
|
|
}
|
|
|
|
// A PolicyEvaluator evaluates policies.
|
|
type PolicyEvaluator struct {
|
|
queries []policyQuery
|
|
}
|
|
|
|
// NewPolicyEvaluator creates a new PolicyEvaluator.
|
|
func NewPolicyEvaluator(ctx context.Context, store *Store, configPolicy *config.Policy) (*PolicyEvaluator, error) {
|
|
e := new(PolicyEvaluator)
|
|
|
|
// generate the base rego script for the policy
|
|
ppl := configPolicy.ToPPL()
|
|
base, err := policy.GenerateRegoFromPolicy(ppl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scripts := []string{base}
|
|
|
|
// add any custom rego
|
|
for _, sp := range configPolicy.SubPolicies {
|
|
for _, src := range sp.Rego {
|
|
if src == "" {
|
|
continue
|
|
}
|
|
|
|
scripts = append(scripts, src)
|
|
}
|
|
}
|
|
|
|
// for each script, create a rego and prepare a query.
|
|
for _, script := range scripts {
|
|
log.Debug(ctx).
|
|
Str("script", script).
|
|
Str("from", configPolicy.From).
|
|
Interface("to", configPolicy.To).
|
|
Msg("authorize: rego script for policy evaluation")
|
|
|
|
r := rego.New(
|
|
rego.Store(store),
|
|
rego.Module("pomerium.policy", script),
|
|
rego.Query("result = data.pomerium.policy"),
|
|
getGoogleCloudServerlessHeadersRegoOption,
|
|
store.GetDataBrokerRecordOption(),
|
|
)
|
|
|
|
q, err := r.PrepareForEval(ctx)
|
|
// if no package is in the src, add it
|
|
if err != nil && strings.Contains(err.Error(), "package expected") {
|
|
r := rego.New(
|
|
rego.Store(store),
|
|
rego.Module("pomerium.policy", "package pomerium.policy\n\n"+script),
|
|
rego.Query("result = data.pomerium.policy"),
|
|
getGoogleCloudServerlessHeadersRegoOption,
|
|
store.GetDataBrokerRecordOption(),
|
|
)
|
|
q, err = r.PrepareForEval(ctx)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
e.queries = append(e.queries, policyQuery{
|
|
PreparedEvalQuery: q,
|
|
checksum: fmt.Sprintf("%x", cryptutil.Hash("script", []byte(script))),
|
|
})
|
|
}
|
|
|
|
return e, nil
|
|
}
|
|
|
|
// Evaluate evaluates the policy rego scripts.
|
|
func (e *PolicyEvaluator) Evaluate(ctx context.Context, req *PolicyRequest) (*PolicyResponse, error) {
|
|
res := new(PolicyResponse)
|
|
// run each query and merge the results
|
|
for _, query := range e.queries {
|
|
o, err := e.evaluateQuery(ctx, req, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = res.Merge(o)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (e *PolicyEvaluator) evaluateQuery(ctx context.Context, req *PolicyRequest, query policyQuery) (*PolicyResponse, error) {
|
|
_, span := trace.StartSpan(ctx, "authorize.PolicyEvaluator.evaluateQuery")
|
|
defer span.End()
|
|
span.AddAttributes(octrace.StringAttribute("script_checksum", query.checksum))
|
|
|
|
rs, err := safeEval(ctx, query.PreparedEvalQuery, rego.EvalInput(req))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authorize: error evaluating policy.rego: %w", err)
|
|
}
|
|
|
|
if len(rs) == 0 {
|
|
return nil, fmt.Errorf("authorize: unexpected empty result from evaluating policy.rego")
|
|
}
|
|
|
|
res := &PolicyResponse{
|
|
Allow: e.getAllow(rs[0].Bindings),
|
|
Deny: e.getDeny(ctx, rs[0].Bindings),
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// getAllow gets the allow var. It expects a boolean.
|
|
func (e *PolicyEvaluator) getAllow(vars rego.Vars) bool {
|
|
m, ok := vars["result"].(map[string]interface{})
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
allow, ok := m["allow"].(bool)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return allow
|
|
}
|
|
|
|
// getDeny gets the deny var. It expects an (http status code, message) pair.
|
|
func (e *PolicyEvaluator) getDeny(ctx context.Context, vars rego.Vars) *Denial {
|
|
m, ok := vars["result"].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var status int
|
|
var reason string
|
|
switch t := m["deny"].(type) {
|
|
case bool:
|
|
if t {
|
|
status = http.StatusForbidden
|
|
reason = ""
|
|
} else {
|
|
return nil
|
|
}
|
|
case []interface{}:
|
|
switch len(t) {
|
|
case 0:
|
|
return nil
|
|
case 2:
|
|
var err error
|
|
status, err = strconv.Atoi(fmt.Sprint(t[0]))
|
|
if err != nil {
|
|
log.Error(ctx).Err(err).Msg("invalid type in deny")
|
|
return nil
|
|
}
|
|
reason = fmt.Sprint(t[1])
|
|
default:
|
|
log.Error(ctx).Interface("deny", t).Msg("invalid size in deny")
|
|
return nil
|
|
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
return &Denial{
|
|
Status: status,
|
|
Message: reason,
|
|
}
|
|
}
|