Bind service task HTTP input to authenticated caller before evaluation
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Failing after 1m1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 16:25:55 +02:00
parent 919ce25a37
commit 4f6946bbb3
8 changed files with 590 additions and 9 deletions

View file

@ -1,20 +1,27 @@
package taskexecution
import (
"bytes"
"context"
"encoding/json"
"errors"
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/pkg/api"
"io"
"time"
)
var ErrServiceInput = errors.New("service task execution input refused")
const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot"
const ServicePilotPackageVersion = "v1"
const ServicePilotMappingVersion = "proposed-service-task-caller/v1"
const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine"
// ServicePilotInputs is source preparation only. It installs the explicit
// ServicePilotInputs installs the exact B1 service input mapping for signed DENY.
// It does not validate Approval facts or establish executable eligibility.
// It installs the explicit
// proposed fixed subject-to-transport mapping, never a request-selected mapping.
// No constructor parameter can select warn/disabled mode or another subject.
type ServicePilotInputs struct{ auth *callerauth.Authenticator }
@ -47,6 +54,44 @@ func (s ServiceInput) Request() (api.CheckRequest, error) {
}
func (s ServiceInput) Caller() callerauth.Record { return s.caller }
// RequestAt rechecks transport and operation lifetime at the runtime use site.
// Request() remains a detached source snapshot, never an execution capability.
func (s ServiceInput) RequestAt(now time.Time) (api.CheckRequest, error) {
r, err := s.Request()
if err != nil || now.IsZero() || s.caller.Audience != "flex-auth" || !s.caller.NotAfter.After(now) {
return api.CheckRequest{}, ErrServiceInput
}
contract, ok := r.Context["execution_contract"].(map[string]any)
if !ok {
return api.CheckRequest{}, ErrServiceInput
}
value, ok := contract["operation_deadline"].(string)
if !ok {
return api.CheckRequest{}, ErrServiceInput
}
deadline, err := time.Parse(time.RFC3339Nano, value)
if err != nil || !deadline.After(now) || deadline.After(s.caller.NotAfter) {
return api.CheckRequest{}, ErrServiceInput
}
return r, nil
}
// DecodeServiceRequest reuses the owner strict JSON scanner for this bounded
// task-only route. It never changes producer canonicalization or legacy input.
func DecodeServiceRequest(reader io.Reader) (api.CheckRequest, error) {
raw, err := io.ReadAll(io.LimitReader(reader, 524289))
var request api.CheckRequest
if err != nil || len(raw) > 524288 || strictObject(raw, &request) != nil {
return request, ErrServiceInput
}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if decoder.Decode(&request) != nil {
return api.CheckRequest{}, ErrServiceInput
}
return request, nil
}
// DeclaredServiceBinding is public immutable metadata to bind at approval issue.
// It is not authentication evidence; Prepare independently verifies its principal.
func DeclaredServiceBinding() map[string]any {