access-engine/internal/taskexecution/service_input.go
tegwick 4f6946bbb3
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
Bind service task HTTP input to authenticated caller before evaluation
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
2026-10-16 16:25:55 +02:00

146 lines
5.8 KiB
Go

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 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 }
func NewServicePilotInputs(reviewer callerauth.TokenReviewer) (*ServicePilotInputs, error) {
a, e := callerauth.New(callerauth.ModeEnforce, reviewer, "flex-auth", map[string]string{"secrets-engine": ServicePilotPrincipal}, nil)
if e != nil {
return nil, ErrServiceInput
}
return &ServicePilotInputs{auth: a}, nil
}
// ServiceInput cannot be fabricated through exported fields. It proves only
// source input preparation through TokenReview, not approval provenance/consume
// or execution authority. Default zero value is not an authenticated input.
type ServiceInput struct {
serialized []byte
caller callerauth.Record
}
func (s ServiceInput) Request() (api.CheckRequest, error) {
if s.caller.Mode != callerauth.ModeEnforce || s.caller.Principal != ServicePilotPrincipal || len(s.serialized) == 0 {
return api.CheckRequest{}, ErrServiceInput
}
var r api.CheckRequest
if json.Unmarshal(s.serialized, &r) != nil {
return r, ErrServiceInput
}
return r, nil
}
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 {
return map[string]any{"version": ServicePilotMappingVersion, "transport_principal": ServicePilotPrincipal, "subject_id": "secrets-engine", "subject_type": "service", "tenant": "tenant:platform"}
}
func (s *ServicePilotInputs) Prepare(ctx context.Context, authorization string, r api.CheckRequest, now time.Time) (ServiceInput, error) {
refuse := func() (ServiceInput, error) { return ServiceInput{}, ErrServiceInput }
if s == nil || s.auth == nil || now.IsZero() || r.Action != "execute-ccr" || r.Tenant != "tenant:platform" || r.Subject.ID != "secrets-engine" || r.Subject.Type != "service" || r.Resource.System != "secrets-engine" || r.Resource.Type != "secret-catalog-lane" || r.Resource.Attributes["stage"] != "prod" || r.Context["human_control"] != true {
return refuse()
}
for _, key := range []string{"task_execution_projection", "initiator", "represented_principal", "delegation", "verified", "owner_admission"} {
if _, found := r.Context[key]; found {
return refuse()
}
}
if _, found := r.Subject.Attributes["principal"]; found {
return refuse()
}
supplied, err := json.Marshal(r.Context["service_actor_binding"])
if err != nil {
return refuse()
}
wanted, _ := json.Marshal(DeclaredServiceBinding())
if string(supplied) != string(wanted) {
return refuse()
}
identity, err := s.auth.Authorize(ctx, authorization, []string{r.Resource.System})
if err != nil {
return refuse()
}
record := s.auth.Record(identity)
if record.Mode != callerauth.ModeEnforce || record.Principal != ServicePilotPrincipal || record.Audience != "flex-auth" || record.NotAfter.IsZero() || !record.NotAfter.After(now) {
return refuse()
}
c, ok := r.Context["execution_contract"].(map[string]any)
if !ok || c["contract_version"] != "SECRETS-EXEC-20261011-v1" || c["operation"] != "metadata-apply-ccr" || c["execution_actor"] != "service:secrets-engine" {
return refuse()
}
deadline, ok := c["operation_deadline"].(string)
if !ok {
return refuse()
}
end, err := time.Parse(time.RFC3339Nano, deadline)
if err != nil || !end.After(now) || end.After(record.NotAfter) {
return refuse()
}
raw, err := json.Marshal(r)
if err != nil {
return refuse()
}
return ServiceInput{serialized: raw, caller: record}, nil
}