Files
Assistant/modules/capability/domain/candidate_binding.go
2026-08-31 16:28:57 +03:00

86 lines
4.3 KiB
Go

package domain
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"net/url"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
var ErrCandidateBindingRequired = errors.New("candidate capability binding required")
type canonicalCandidateTargetDescriptor struct {
Version string `json:"version"`
BindingRevision int64 `json:"binding_revision"`
TargetKind string `json:"target_kind"`
TargetRef string `json:"target_ref"`
Protocol string `json:"protocol"`
Origin string `json:"origin"`
ReleaseSetID string `json:"release_set_id"`
Slot string `json:"slot"`
TaskID string `json:"task_id"`
TaskRevision int64 `json:"task_revision"`
CapabilityID string `json:"capability_id"`
ResourceScope string `json:"resource_scope"`
Mode string `json:"mode"`
}
func CandidateTargetDigest(value capability.CandidateBinding) (string, error) {
origin, err := url.Parse(value.TargetOrigin)
if err != nil || origin.Host == "" || origin.User != nil || origin.RawQuery != "" || origin.Fragment != "" || (origin.Path != "" && origin.Path != "/") {
return "", ErrCandidateBindingRequired
}
origin.Scheme, origin.Host, origin.Path = strings.ToLower(origin.Scheme), strings.ToLower(origin.Host), ""
descriptor := canonicalCandidateTargetDescriptor{Version: "v1", BindingRevision: value.Revision, TargetKind: value.Context.TargetKind, TargetRef: value.Context.TargetRef, Protocol: string(value.Protocol), Origin: origin.String(), ReleaseSetID: value.Context.ReleaseSetID, Slot: value.Context.Slot, TaskID: value.Context.TaskID, TaskRevision: value.Context.TaskRevision, CapabilityID: string(value.Context.CapabilityID), ResourceScope: value.Context.ResourceScope, Mode: string(value.Context.Mode)}
encoded, err := json.Marshal(descriptor)
if err != nil {
return "", err
}
sum := sha256.Sum256(encoded)
return hex.EncodeToString(sum[:]), nil
}
// ValidateCandidateBinding validates Capability-owned candidate state. Stable
// Binding validation intentionally remains stricter and HTTPS-only.
func ValidateCandidateBinding(value capability.CandidateBinding) error {
context := value.Context
if value.ID == "" || value.Revision < 1 || value.BaseActiveBindingID == "" || value.BaseActiveBindingRevision < 1 ||
context.ReleaseSetID == "" || context.Slot == "" || context.TaskID == "" || context.TaskRevision < 1 || context.PrincipalID == "" ||
context.CapabilityID == "" || context.ResourceScope == "" || context.TargetKind == "" || context.TargetRef == "" || len(context.TargetDigest) != 64 || context.AuthorityGrantID == "" ||
(value.Status != capability.CandidateBindingActive && value.Status != capability.CandidateBindingRevoked) ||
(value.Context.Mode != capability.CandidateBindingModeCandidate && value.Context.Mode != capability.CandidateBindingModeShadow) ||
value.Protocol != capability.ProtocolIdempotentReceiptV1 || value.CreatedAtUnixMillis < 1 || value.ExpiresAtUnixMillis <= value.CreatedAtUnixMillis ||
!fixedPath(value.DeliveryPath) || !fixedPath(value.ReceiptPath) || value.DeliveryPath == value.ReceiptPath {
return ErrCandidateBindingRequired
}
origin, err := url.Parse(value.TargetOrigin)
if err != nil || origin.Host == "" || origin.User != nil || origin.RawQuery != "" || origin.Fragment != "" || (origin.Path != "" && origin.Path != "/") {
return ErrCandidateBindingRequired
}
if context.Mode == capability.CandidateBindingModeCandidate && origin.Scheme != "https" {
return ErrCandidateBindingRequired
}
if context.Mode == capability.CandidateBindingModeShadow && origin.Scheme != "http" && origin.Scheme != "https" {
return ErrCandidateBindingRequired
}
if strings.TrimSpace(value.TargetOrigin) != value.TargetOrigin {
return ErrCandidateBindingRequired
}
digest, err := CandidateTargetDigest(value)
if err != nil || digest != context.TargetDigest {
return ErrCandidateBindingRequired
}
return nil
}
func CandidateBindingInvocable(value capability.CandidateBinding, context capability.CandidateReleaseContext, nowUnixMillis int64) error {
if ValidateCandidateBinding(value) != nil || value.Status != capability.CandidateBindingActive || nowUnixMillis < value.CreatedAtUnixMillis || nowUnixMillis >= value.ExpiresAtUnixMillis || value.Context != context {
return ErrCandidateBindingRequired
}
return nil
}