This commit is contained in:
@@ -24,9 +24,25 @@ func ValidateDelegatedControlPlaneGrant(nowUnixMillis int64, binding identity.Pr
|
||||
}
|
||||
|
||||
func RequestMatchesDelegatedControlPlaneGrant(grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest) bool {
|
||||
return grant.TaskID != "" && grant.TaskID == request.TaskID && grant.TaskRevision > 0 && grant.TaskRevision == request.TaskRevision &&
|
||||
request.GrantID == grant.ID && request.Operation != "" && slices.Contains(grant.Operations, request.Operation) &&
|
||||
grant.Resource.BindingID != "" && grant.Resource.ProviderInstanceID != "" && grant.Resource.ModelDescriptorID != "" && grant.Resource == request.Resource
|
||||
if grant.TaskID == "" || grant.TaskID != request.TaskID || grant.TaskRevision < 1 || grant.TaskRevision != request.TaskRevision ||
|
||||
request.GrantID != grant.ID || request.Operation == "" || !slices.Contains(grant.Operations, request.Operation) {
|
||||
return false
|
||||
}
|
||||
switch request.Operation {
|
||||
case authority.DelegatedOperationCreateCandidateCapabilityBinding, authority.DelegatedOperationUseCandidateCapabilityBinding, authority.DelegatedOperationRevokeCandidateCapabilityBinding:
|
||||
return validCandidateResource(grant.CandidateResource) && grant.CandidateResource == request.CandidateResource && request.Resource == (authority.CognitiveBindingResource{})
|
||||
default:
|
||||
return validCognitiveResource(grant.Resource) && grant.Resource == request.Resource && request.CandidateResource == (authority.CandidateCapabilityBindingResource{})
|
||||
}
|
||||
}
|
||||
|
||||
func validCognitiveResource(resource authority.CognitiveBindingResource) bool {
|
||||
return resource.BindingID != "" && resource.ProviderInstanceID != "" && resource.ModelDescriptorID != ""
|
||||
}
|
||||
|
||||
func validCandidateResource(resource authority.CandidateCapabilityBindingResource) bool {
|
||||
return resource.ReleaseSetID != "" && resource.Slot != "" && resource.CapabilityID != "" && resource.ResourceScope != "" &&
|
||||
(resource.Mode == "CANDIDATE" || resource.Mode == "SHADOW") && resource.TargetKind != "" && resource.TargetRef != "" && len(resource.TargetDigest) == 64
|
||||
}
|
||||
|
||||
func CanIssuerRevokeDelegatedGrant(issuer identity.PrincipalID, grant authority.DelegatedControlPlaneGrant) bool {
|
||||
|
||||
@@ -41,3 +41,17 @@ func TestDelegatedGrantCannotSelfRevokeOrBroaden(t *testing.T) {
|
||||
t.Fatal("issuer could not revoke active grant")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateBindingGrantIsExactAndCannotBroaden(t *testing.T) {
|
||||
resource := authority.CandidateCapabilityBindingResource{ReleaseSetID: "assistant-slot-b", Slot: "b", CapabilityID: "prospective-notification-delivery", ResourceScope: "synthetic-shadow-notification-v1", Mode: "SHADOW", TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", TargetDigest: "271c79ca1b1a590f1309461a87ba39bc017dfeb8d99268d1a7bd3a16d1d4b4d3"}
|
||||
grant := authority.DelegatedControlPlaneGrant{ID: "candidate-grant", ServiceActorID: "service-actor", ServicePrincipalID: "service-principal", IssuerPrincipalID: "owner", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 2, Operations: []authority.DelegatedControlPlaneOperation{authority.DelegatedOperationUseCandidateCapabilityBinding}, CandidateResource: resource, IssuedAtUnixMillis: 10, ExpiresAtUnixMillis: 100, ArchitectureRevision: "ADR-0021", Nonce: "nonce", Revision: 1, Status: authority.DelegatedGrantActive}
|
||||
binding := identity.PrincipalBinding{ActorID: "service-actor", PrincipalID: "service-principal", Active: true}
|
||||
request := authority.DelegatedControlPlaneRequest{GrantID: grant.ID, TaskID: grant.TaskID, TaskRevision: grant.TaskRevision, Operation: authority.DelegatedOperationUseCandidateCapabilityBinding, CandidateResource: resource}
|
||||
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err != nil {
|
||||
t.Fatalf("exact candidate grant denied: %v", err)
|
||||
}
|
||||
request.CandidateResource.ReleaseSetID = "assistant-slot-a"
|
||||
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err == nil {
|
||||
t.Fatal("different release set was allowed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
func validCandidateBinding() capability.CandidateBinding {
|
||||
context := capability.CandidateReleaseContext{ReleaseSetID: "assistant-slot-b", Slot: "b", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 7, PrincipalID: identity.PrincipalID("owner-1"), CapabilityID: "prospective-notification-delivery", ResourceScope: "synthetic-shadow-notification-v1", Mode: capability.CandidateBindingModeShadow, TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", AuthorityGrantID: "grant-1"}
|
||||
binding := capability.CandidateBinding{ID: "candidate-binding-1", Revision: 1, BaseActiveBindingID: "stable-binding-1", BaseActiveBindingRevision: 1, Context: context, TargetOrigin: "http://notification-receiver-candidate:8085", DeliveryPath: "/delivery/v1/deliveries", ReceiptPath: "/delivery/v1/receipts", Protocol: capability.ProtocolIdempotentReceiptV1, CreatedAtUnixMillis: 10, ExpiresAtUnixMillis: 100, Status: capability.CandidateBindingActive}
|
||||
digest, err := CandidateTargetDigest(binding)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
binding.Context.TargetDigest = digest
|
||||
return binding
|
||||
}
|
||||
|
||||
func TestCandidateBindingIsExactAndFailsClosed(t *testing.T) {
|
||||
binding := validCandidateBinding()
|
||||
if err := CandidateBindingInvocable(binding, binding.Context, 20); err != nil {
|
||||
t.Fatalf("valid candidate binding denied: %v", err)
|
||||
}
|
||||
for _, mutate := range []func(*capability.CandidateReleaseContext){
|
||||
func(v *capability.CandidateReleaseContext) { v.ReleaseSetID = "assistant-slot-other" },
|
||||
func(v *capability.CandidateReleaseContext) { v.TaskID = "GEN2-OTHER" },
|
||||
func(v *capability.CandidateReleaseContext) { v.AuthorityGrantID = "other-grant" },
|
||||
} {
|
||||
request := binding.Context
|
||||
mutate(&request)
|
||||
if err := CandidateBindingInvocable(binding, request, 20); err == nil {
|
||||
t.Fatal("mismatched candidate context was allowed")
|
||||
}
|
||||
}
|
||||
if err := CandidateBindingInvocable(binding, binding.Context, 100); err == nil {
|
||||
t.Fatal("expired candidate binding was allowed")
|
||||
}
|
||||
binding.Status = capability.CandidateBindingRevoked
|
||||
if err := CandidateBindingInvocable(binding, binding.Context, 20); err == nil {
|
||||
t.Fatal("revoked candidate binding was allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateTargetDigestBindsSemanticDescriptor(t *testing.T) {
|
||||
binding := validCandidateBinding()
|
||||
digest, err := CandidateTargetDigest(binding)
|
||||
if err != nil || digest != binding.Context.TargetDigest {
|
||||
t.Fatalf("digest=%q err=%v", digest, err)
|
||||
}
|
||||
if digest != "f9f9fa49a0727444c280c092d5bb340f3a8f33cf325c5396f5ae177a6666da35" {
|
||||
t.Fatalf("canonical descriptor digest changed: %q", digest)
|
||||
}
|
||||
changed := binding
|
||||
changed.Context.ResourceScope = "different-resource"
|
||||
other, err := CandidateTargetDigest(changed)
|
||||
if err != nil || other == digest {
|
||||
t.Fatalf("scope did not affect digest: %q %q err=%v", digest, other, err)
|
||||
}
|
||||
changed = binding
|
||||
changed.Revision++
|
||||
other, err = CandidateTargetDigest(changed)
|
||||
if err != nil || other == digest {
|
||||
t.Fatalf("binding revision did not affect digest: %q %q err=%v", digest, other, err)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ type Repository interface {
|
||||
SaveBinding(context.Context, capability.Binding) error
|
||||
LoadBinding(context.Context, capability.CapabilityBindingID, int64) (capability.Binding, error)
|
||||
LoadActiveBinding(context.Context, capability.CapabilityID, identity.PrincipalID) (capability.Binding, error)
|
||||
SaveCandidateBinding(context.Context, capability.CandidateBinding) error
|
||||
ResolveCandidateBinding(context.Context, capability.CandidateReleaseContext, int64) (capability.CandidateBindingResolution, error)
|
||||
RevokeCandidateBinding(context.Context, capability.CapabilityBindingID, int64, int64) (bool, error)
|
||||
SaveProviderInstance(context.Context, capability.ProviderInstance) error
|
||||
SaveModelDescriptor(context.Context, capability.ModelDescriptor) error
|
||||
SaveCognitiveBinding(context.Context, capability.CognitiveBinding) error
|
||||
|
||||
@@ -98,19 +98,26 @@ func validDigest(value string) bool {
|
||||
// Execute records UNKNOWN_OUTCOME rather than silently retrying an ambiguous
|
||||
// external mutation.
|
||||
func Execute(action commitcontract.AuthorizedAction, attemptID commitcontract.ActionAttemptID, executor ports.Executor) (commitcontract.ActionAttempt, error) {
|
||||
attempt, _, err := ExecuteWithResult(action, attemptID, executor)
|
||||
return attempt, err
|
||||
}
|
||||
|
||||
// ExecuteWithResult preserves executor evidence for the Commit runtime while
|
||||
// keeping the original domain API compatible with existing callers.
|
||||
func ExecuteWithResult(action commitcontract.AuthorizedAction, attemptID commitcontract.ActionAttemptID, executor ports.Executor) (commitcontract.ActionAttempt, ports.ExecutionResult, error) {
|
||||
if action.State != commitcontract.ActionAuthorized || attemptID == "" {
|
||||
return commitcontract.ActionAttempt{}, ErrUnknownOutcome
|
||||
return commitcontract.ActionAttempt{}, ports.ExecutionResult{}, ErrUnknownOutcome
|
||||
}
|
||||
attempt := commitcontract.ActionAttempt{ID: attemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting}
|
||||
result := executor.Execute(action)
|
||||
if !result.Known {
|
||||
attempt.State = commitcontract.ActionUnknownOutcome
|
||||
return attempt, ErrUnknownOutcome
|
||||
return attempt, result, ErrUnknownOutcome
|
||||
}
|
||||
if result.Succeeded {
|
||||
attempt.State = commitcontract.ActionSucceeded
|
||||
return attempt, nil
|
||||
return attempt, result, nil
|
||||
}
|
||||
attempt.State = commitcontract.ActionFailed
|
||||
return attempt, nil
|
||||
return attempt, result, nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type Reconciler interface {
|
||||
type ExecutionResult struct {
|
||||
Succeeded bool
|
||||
Known bool
|
||||
Receipt *commit.ExecutionReceipt
|
||||
}
|
||||
|
||||
// AuthorityValidator is the remote-capable boundary to the Authority owner.
|
||||
|
||||
Reference in New Issue
Block a user