Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
215 lines
8 KiB
Go
215 lines
8 KiB
Go
package taskexecution
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/netkingdom/access-engine/pkg/api"
|
|
"github.com/netkingdom/approval-engine/adapters/go/approvalfact"
|
|
)
|
|
|
|
var ErrApprovalSource = errors.New("trusted Approval source refused")
|
|
var approvalUUID = regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$`)
|
|
|
|
type ApprovalSourceConfig struct {
|
|
SourceID string `json:"source_id"`
|
|
SourceRevision string `json:"source_revision"`
|
|
Origin string `json:"origin"`
|
|
StoreTenant string `json:"store_tenant"`
|
|
CAFile string `json:"ca_file"`
|
|
TokenFile string `json:"token_file"`
|
|
}
|
|
|
|
// ApprovalReader is installed from owner configuration, never an HTTP request.
|
|
// It reads one fixed PIP endpoint; no policy, retry, consume or effect lives here.
|
|
type ApprovalReader struct {
|
|
config ApprovalSourceConfig
|
|
configurationDigest string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewApprovalReader(config ApprovalSourceConfig) (*ApprovalReader, error) {
|
|
u, err := url.Parse(config.Origin)
|
|
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") || strings.ContainsAny(config.Origin, "\r\n\t ") || config.SourceID == "" || config.SourceRevision == "" || config.StoreTenant != "tenant:platform" || !filepath.IsAbs(config.CAFile) || !filepath.IsAbs(config.TokenFile) {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
ca, err := readPublicApprovalFile(config.CAFile, 1048576)
|
|
if err != nil {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(ca) {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
config.Origin = strings.TrimRight(config.Origin, "/")
|
|
digest := api.CanonicalDigest(struct {
|
|
Config ApprovalSourceConfig `json:"configuration"`
|
|
CA string `json:"ca_sha256"`
|
|
}{config, byteDigest(ca)})
|
|
return &ApprovalReader{config: config, configurationDigest: digest, client: &http.Client{Timeout: 3 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, Transport: &http.Transport{Proxy: nil, DisableCompression: true, TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: pool}}}}, nil
|
|
}
|
|
func readPublicApprovalFile(path string, limit int64) ([]byte, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0022 != 0 {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
raw, err := io.ReadAll(io.LimitReader(file, limit+1))
|
|
if err != nil || int64(len(raw)) > limit {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
return raw, nil
|
|
}
|
|
func LoadApprovalReader(path string) (*ApprovalReader, error) {
|
|
if !filepath.IsAbs(path) {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
raw, err := readPublicApprovalFile(path, 65536)
|
|
if err != nil {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
if _, err := approvalfact.DecodeJSON(raw); err != nil {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
var config ApprovalSourceConfig
|
|
decoder := json.NewDecoder(strings.NewReader(string(raw)))
|
|
decoder.DisallowUnknownFields()
|
|
if decoder.Decode(&config) != nil {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
return NewApprovalReader(config)
|
|
}
|
|
func (r *ApprovalReader) Configured() bool {
|
|
return r != nil && r.client != nil && r.configurationDigest != ""
|
|
}
|
|
func byteDigest(raw []byte) string {
|
|
h := sha256.Sum256(raw)
|
|
return "sha256:" + hex.EncodeToString(h[:])
|
|
}
|
|
func readApprovalToken(path string) (string, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return "", ErrApprovalSource
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0027 != 0 {
|
|
return "", ErrApprovalSource
|
|
}
|
|
raw, err := io.ReadAll(io.LimitReader(file, 8193))
|
|
if err != nil || len(raw) > 8192 {
|
|
return "", ErrApprovalSource
|
|
}
|
|
token := strings.TrimSpace(string(raw))
|
|
if token == "" || strings.ContainsAny(token, " \t\r\n") {
|
|
return "", ErrApprovalSource
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
// ApprovalObservation cannot be created by caller JSON or a public verified flag.
|
|
// It is raw fact data plus the source/configuration used by this fixed reader.
|
|
type ApprovalObservation struct {
|
|
raw []byte
|
|
sourceID, revision, configurationDigest string
|
|
}
|
|
|
|
func (r *ApprovalReader) Observe(ctx context.Context, id string) (ApprovalObservation, error) {
|
|
if !r.Configured() || !approvalUUID.MatchString(id) {
|
|
return ApprovalObservation{}, ErrApprovalSource
|
|
}
|
|
token, err := readApprovalToken(r.config.TokenFile)
|
|
if err != nil {
|
|
return ApprovalObservation{}, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.config.Origin+"/v1/approvals/"+id+"/claim", nil)
|
|
if err != nil {
|
|
return ApprovalObservation{}, ErrApprovalSource
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Accept", "application/json")
|
|
response, err := r.client.Do(req)
|
|
if err != nil {
|
|
return ApprovalObservation{}, ErrApprovalSource
|
|
}
|
|
defer response.Body.Close()
|
|
types := response.Header.Values("Content-Type")
|
|
if len(types) != 1 {
|
|
return ApprovalObservation{}, ErrApprovalSource
|
|
}
|
|
media, parameters, err := mime.ParseMediaType(types[0])
|
|
if err != nil || media != "application/json" || (parameters["charset"] != "" && !strings.EqualFold(parameters["charset"], "utf-8")) || (len(parameters) != 0 && (len(parameters) != 1 || parameters["charset"] == "")) || response.StatusCode != 200 || response.Header.Get("Content-Encoding") != "" {
|
|
return ApprovalObservation{}, ErrApprovalSource
|
|
}
|
|
raw, err := io.ReadAll(io.LimitReader(response.Body, 524289))
|
|
if err != nil || len(raw) > 524288 {
|
|
return ApprovalObservation{}, ErrApprovalSource
|
|
}
|
|
if _, err := approvalfact.DecodeJSON(raw); err != nil {
|
|
return ApprovalObservation{}, ErrApprovalSource
|
|
}
|
|
return ApprovalObservation{raw: append([]byte(nil), raw...), sourceID: r.config.SourceID, revision: r.config.SourceRevision, configurationDigest: r.configurationDigest}, nil
|
|
}
|
|
|
|
// VerifiedApprovalFacts establishes typed fact correspondence, not a decision.
|
|
// Only an authenticated fixed-reader observation can construct this handle.
|
|
type VerifiedApprovalFacts struct {
|
|
original, fetched approvalfact.Fact
|
|
evidence api.ApprovalObservationProvenance
|
|
valid bool
|
|
}
|
|
|
|
func (o ApprovalObservation) Validate(original []byte, expected approvalfact.Expected, now time.Time) (VerifiedApprovalFacts, error) {
|
|
if len(o.raw) == 0 || o.sourceID == "" || o.revision == "" || o.configurationDigest == "" {
|
|
return VerifiedApprovalFacts{}, ErrApprovalSource
|
|
}
|
|
originalFact, err := approvalfact.ValidateClaim(original, expected, now)
|
|
if err != nil {
|
|
return VerifiedApprovalFacts{}, ErrApprovalSource
|
|
}
|
|
fetched, err := approvalfact.ValidateClaim(o.raw, expected, now)
|
|
if err != nil || approvalfact.MatchObserved(originalFact, fetched) != nil {
|
|
return VerifiedApprovalFacts{}, ErrApprovalSource
|
|
}
|
|
limit := originalFact.Deadline()
|
|
if fetched.Deadline().Before(limit) {
|
|
limit = fetched.Deadline()
|
|
}
|
|
evidence := api.ApprovalObservationProvenance{SourceID: o.sourceID, SourceRevision: o.revision, ConfigurationDigest: o.configurationDigest, ClaimDigest: byteDigest(o.raw), NativeBindingDigest: fetched.NativeDigest(), ObservedAt: fetched.ObservedAt().UTC().Format(time.RFC3339Nano), NotAfter: limit.UTC().Format(time.RFC3339Nano), ValidatedAt: now.UTC().Format(time.RFC3339Nano)}
|
|
return VerifiedApprovalFacts{original: originalFact, fetched: fetched, evidence: evidence, valid: true}, nil
|
|
}
|
|
func (f VerifiedApprovalFacts) Deadline() (time.Time, error) {
|
|
if !f.valid {
|
|
return time.Time{}, ErrApprovalSource
|
|
}
|
|
a := f.original.Deadline()
|
|
if f.fetched.Deadline().Before(a) {
|
|
a = f.fetched.Deadline()
|
|
}
|
|
return a, nil
|
|
}
|
|
func (f VerifiedApprovalFacts) Provenance() (*api.ApprovalObservationProvenance, error) {
|
|
if !f.valid {
|
|
return nil, ErrApprovalSource
|
|
}
|
|
copy := f.evidence
|
|
return ©, nil
|
|
}
|