mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 02:16:28 +02:00
242 lines
6.1 KiB
Go
242 lines
6.1 KiB
Go
package evaluator
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"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"
|
|
"github.com/pomerium/pomerium/pkg/policy/criteria"
|
|
)
|
|
|
|
// 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, Deny RuleResult
|
|
}
|
|
|
|
// NewPolicyResponse creates a new PolicyResponse.
|
|
func NewPolicyResponse() *PolicyResponse {
|
|
return &PolicyResponse{
|
|
Allow: NewRuleResult(false),
|
|
Deny: NewRuleResult(false),
|
|
}
|
|
}
|
|
|
|
// A RuleResult is the result of evaluating a rule.
|
|
type RuleResult struct {
|
|
Value bool
|
|
Reasons criteria.Reasons
|
|
AdditionalData map[string]interface{}
|
|
}
|
|
|
|
// NewRuleResult creates a new RuleResult.
|
|
func NewRuleResult(value bool, reasons ...criteria.Reason) RuleResult {
|
|
return RuleResult{
|
|
Value: value,
|
|
Reasons: criteria.NewReasons(reasons...),
|
|
AdditionalData: map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
// MergeRuleResultsWithOr merges all the results using `or`.
|
|
func MergeRuleResultsWithOr(results ...RuleResult) RuleResult {
|
|
merged := NewRuleResult(false)
|
|
|
|
var trueResults, falseResults []RuleResult
|
|
for _, result := range results {
|
|
if result.Value {
|
|
trueResults = append(trueResults, result)
|
|
} else {
|
|
falseResults = append(falseResults, result)
|
|
}
|
|
}
|
|
|
|
if len(trueResults) > 0 {
|
|
merged.Value = true
|
|
for _, result := range trueResults {
|
|
merged.Reasons = merged.Reasons.Union(result.Reasons)
|
|
for k, v := range result.AdditionalData {
|
|
merged.AdditionalData[k] = v
|
|
}
|
|
}
|
|
} else {
|
|
merged.Value = false
|
|
for _, result := range falseResults {
|
|
merged.Reasons = merged.Reasons.Union(result.Reasons)
|
|
for k, v := range result.AdditionalData {
|
|
merged.AdditionalData[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
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 := NewPolicyResponse()
|
|
// 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.Allow = MergeRuleResultsWithOr(res.Allow, o.Allow)
|
|
res.Deny = MergeRuleResultsWithOr(res.Deny, o.Deny)
|
|
}
|
|
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.getRuleResult("allow", rs[0].Bindings),
|
|
Deny: e.getRuleResult("deny", rs[0].Bindings),
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// getRuleResult gets the rule result var. It expects a boolean, [boolean, []string] or [boolean, []string, object].
|
|
func (e *PolicyEvaluator) getRuleResult(name string, vars rego.Vars) (result RuleResult) {
|
|
result = NewRuleResult(false)
|
|
|
|
m, ok := vars["result"].(map[string]interface{})
|
|
if !ok {
|
|
return result
|
|
}
|
|
|
|
switch t := m[name].(type) {
|
|
case bool:
|
|
result.Value = t
|
|
case []interface{}:
|
|
switch len(t) {
|
|
case 3:
|
|
v, ok := t[2].(map[string]interface{})
|
|
if ok {
|
|
for k, vv := range v {
|
|
result.AdditionalData[k] = vv
|
|
}
|
|
}
|
|
fallthrough
|
|
case 2:
|
|
// fill in the reasons
|
|
v, ok := t[1].([]interface{})
|
|
if ok {
|
|
for _, vv := range v {
|
|
result.Reasons.Add(criteria.Reason(fmt.Sprint(vv)))
|
|
}
|
|
}
|
|
fallthrough
|
|
case 1:
|
|
// fill in the value
|
|
v, ok := t[0].(bool)
|
|
if !ok {
|
|
return result
|
|
}
|
|
result.Value = v
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|