~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
180 lines · 4.9 KB Raw History
package runs

import (
	"encoding/json"
	"fmt"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	policyv1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/policy/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/policy"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/executor"
)

type Decision = policy.Verdict

type Parser struct {
	Bool func(json.RawMessage) (bool, bool)

	Strings func(json.RawMessage) []string
}

var Strict = Parser{Bool: strictBool, Strings: singleString}

func Decide(run *policyv1alpha1.PolicyWorkflowRun, parser Parser) Decision {
	if run == nil {
		return Decision{Reason: "the policy run returned no status"}
	}
	name := run.Name
	switch run.Status.Phase {
	case policyv1alpha1.PolicyWorkflowRunSucceeded:
	case policyv1alpha1.PolicyWorkflowRunFailed:
		return Decision{Reason: failure(run), Run: name}
	case policyv1alpha1.PolicyWorkflowRunCancelled:
		return Decision{Reason: "the policy run was cancelled", Run: name}
	default:
		return Decision{Reason: fmt.Sprintf("the policy run ended in phase %q", run.Status.Phase), Run: name}
	}
	if run.Status.ExitStatus != "" && run.Status.ExitStatus != policyv1alpha1.PolicyExitStatusSuccess {
		return Decision{Reason: "the policy workflow failed", Run: name}
	}
	allow, ok := outputBool(run.Status.Outputs, "allow", parser.Bool)
	if !ok {
		return Decision{Reason: "the policy run produced no allow output", Run: name}
	}
	violations := outputStrings(run.Status.Outputs, "violations", parser.Strings)
	if !allow {
		reason := rejection(run.Status.Outputs)
		if len(violations) > 0 {
			reason += ": " + strings.Join(violations, ", ")
		}
		return Decision{Violations: violations, Reason: reason, Run: name, Completed: true}
	}
	return Decision{Allow: true, Violations: violations, Run: name, Completed: true}
}

func failure(run *policyv1alpha1.PolicyWorkflowRun) string {
	for _, condition := range run.Status.Conditions {
		if condition.Status == metav1.ConditionTrue && condition.Message != "" {
			return condition.Message
		}
	}
	if raw, ok := run.Status.Annotations["error"]; ok && len(raw) > 0 {
		if message := annotationMessage(raw); message != "" {
			return message
		}
		return strings.TrimSpace(string(raw))
	}
	return "the policy run failed"
}

func annotationMessage(raw json.RawMessage) string {
	var envelope struct {
		Items []map[string]any `json:"items"`
	}
	items := []map[string]any{}
	if err := json.Unmarshal(raw, &envelope); err == nil && len(envelope.Items) > 0 {
		items = envelope.Items
	} else if err := json.Unmarshal(raw, &items); err != nil {
		return ""
	}
	for _, item := range items {
		for _, key := range []string{"message", "title", "msg"} {
			if text, ok := item[key].(string); ok && text != "" {
				return text
			}
		}
	}
	return ""
}

func outputBool(outputs map[string]json.RawMessage, key string, boolOf func(json.RawMessage) (bool, bool)) (bool, bool) {
	values := outputValues(outputs, key)
	found := false
	allow := true
	for _, raw := range values {
		found = true
		if value, ok := boolOf(raw); !ok || !value {
			allow = false
		}
	}
	if !found {
		return false, false
	}
	return allow, true
}

func outputStrings(outputs map[string]json.RawMessage, key string, stringsOf func(json.RawMessage) []string) []string {
	var out []string
	for _, raw := range outputValues(outputs, key) {
		out = append(out, stringsOf(raw)...)
	}
	return out
}

func outputValues(outputs map[string]json.RawMessage, key string) []json.RawMessage {
	values := make([]json.RawMessage, 0, len(outputs))
	if raw, ok := outputs[key]; ok && len(raw) > 0 {
		values = append(values, raw)
	}
	for name, raw := range outputs {
		if name == key || len(raw) == 0 {
			continue
		}
		var job map[string]json.RawMessage
		if json.Unmarshal(raw, &job) != nil {
			continue
		}
		if value, ok := job[key]; ok && len(value) > 0 {
			values = append(values, value)
		}
	}
	return values
}

func rejection(outputs map[string]json.RawMessage) string {
	var verdict executor.Verdict
	for _, raw := range outputValues(outputs, "violations") {
		var text string
		if json.Unmarshal(raw, &text) == nil {
			raw = json.RawMessage(text)
		}
		var violations []executor.Violation
		if json.Unmarshal(raw, &violations) == nil {
			verdict.Violations = append(verdict.Violations, violations...)
		}
	}
	return executor.RejectionError(verdict)
}

func strictBool(raw json.RawMessage) (bool, bool) {
	var value bool
	if json.Unmarshal(raw, &value) == nil {
		return value, true
	}
	var text string
	if json.Unmarshal(raw, &text) == nil {
		switch strings.ToLower(strings.TrimSpace(text)) {
		case "true":
			return true, true
		case "false":
			return false, true
		}
	}
	return false, false
}

func singleString(raw json.RawMessage) []string {
	var list []string
	if json.Unmarshal(raw, &list) == nil {
		return list
	}
	var text string
	if json.Unmarshal(raw, &text) == nil {
		if trimmed := strings.TrimSpace(text); trimmed != "" && trimmed != "[]" {
			return []string{trimmed}
		}
	}
	return nil
}