86 lines
4.3 KiB
Go
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
|
|
}
|