Files
oc-lib/models/resources/exploitation_authorization.go
T

224 lines
8.8 KiB
Go
Raw Normal View History

2026-05-27 15:50:23 +02:00
package resources
// exploitation_authorization.go — Autorisation d'Exploitation (AE)
//
// AEs are embedded inside AbstractResource (field ExploitationAuthorizations).
// They are NOT a separate MongoDB collection — each resource document carries
// its own AEs, just like it carries its Instances.
//
// # Visibility filtering
//
// When a resource is returned to a consumer peer the AE list is filtered:
// - AllowedPeerIDs empty → public AE, visible to all peers.
// - AllowedPeerIDs non-empty, contains requester → visible to that peer.
// - AllowedPeerIDs non-empty, doesn't contain requester → stripped from response.
//
// The resource owner always sees all of their own AEs unfiltered.
//
// # Enforcement
//
// oc-schedulerd's validateWorkflowIntegrity calls CheckWorkflowAE (defined in
// its own package to avoid circular imports) before launching any execution.
// Violations emit PEER_BEHAVIOR_EVENT(BehaviorFraud) against the consumer peer
// and cause the execution to be rejected.
import (
"encoding/json"
"fmt"
"slices"
"time"
"cloud.o-forge.io/core/oc-lib/tools"
)
// CouplingConstraint defines which resources must or must not co-exist in the
// same workflow when the protected resource is included.
type CouplingConstraint struct {
// RequiredResourceIDs — ALL of these resource UUIDs must appear in the
// workflow alongside the protected resource.
RequiredResourceIDs []string `json:"required_resource_ids,omitempty" bson:"required_resource_ids,omitempty"`
// ForbiddenResourceIDs — NONE of these resource UUIDs may appear in the
// workflow alongside the protected resource.
ForbiddenResourceIDs []string `json:"forbidden_resource_ids,omitempty" bson:"forbidden_resource_ids,omitempty"`
}
// ExploitationAuthorization (AE) is embedded in a resource and restricts how
// the resource may be used by other consumer peers.
//
// It is stored as part of the resource document (bson embedded), not as a
// separate collection. Create/update it by PATCHing the parent resource.
type ExploitationAuthorization struct {
// ID is a client-assigned UUID so individual AEs can be referenced.
ID string `json:"id" bson:"id"`
// Name is a human-readable label shown in the catalog detail view.
Name string `json:"name,omitempty" bson:"name,omitempty"`
// AllowedPeerIDs restricts which consumer peers may use the resource.
// An empty list means any peer is allowed.
AllowedPeerIDs []string `json:"allowed_peer_ids,omitempty" bson:"allowed_peer_ids,omitempty"`
// AllowedWorkflowIDs restricts which workflow IDs may include the resource.
// An empty list means any workflow is allowed.
AllowedWorkflowIDs []string `json:"allowed_workflow_ids,omitempty" bson:"allowed_workflow_ids,omitempty"`
// Coupling describes positive (required) and negative (forbidden) coupling.
// Nil means no coupling constraint.
Coupling *CouplingConstraint `json:"coupling,omitempty" bson:"coupling,omitempty"`
// ValidFrom / ValidUntil define the active window.
ValidFrom *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`
// IsRevoked allows instant revocation without deleting the AE from the resource.
IsRevoked bool `json:"is_revoked" bson:"is_revoked"`
}
// IsVisibleTo returns true when this AE should be included in the response to
// peerID. The resource owner (creatorID) always sees all AEs.
func (ae *ExploitationAuthorization) IsVisibleTo(peerID, creatorID string) bool {
if peerID == creatorID {
return true // owner sees everything
}
return len(ae.AllowedPeerIDs) == 0 || slices.Contains(ae.AllowedPeerIDs, peerID)
}
// CheckAE evaluates this AE against the execution context and returns any
// violations found. workflowResourceIDs is the set of all resource UUIDs in
// the workflow; resourceID is the UUID of the resource this AE belongs to.
func (ae *ExploitationAuthorization) CheckAE(
resourceID, workflowID, consumerPeerID string,
workflowResourceIDs map[string]struct{},
now time.Time,
) []AEViolation {
var vs []AEViolation
add := func(t AEViolationType, msg string) {
vs = append(vs, AEViolation{AEID: ae.ID, ResourceID: resourceID, Type: t, Message: msg})
}
if ae.IsRevoked {
add(AEViolationRevoked, fmt.Sprintf("AE %s for resource %s is revoked", ae.ID, resourceID))
return vs
}
if ae.ValidUntil != nil && now.After(*ae.ValidUntil) {
add(AEViolationExpired, fmt.Sprintf("AE %s for resource %s expired at %s",
ae.ID, resourceID, ae.ValidUntil.Format(time.RFC3339)))
return vs
}
if ae.ValidFrom != nil && now.Before(*ae.ValidFrom) {
add(AEViolationNotYetValid, fmt.Sprintf("AE %s for resource %s not valid until %s",
ae.ID, resourceID, ae.ValidFrom.Format(time.RFC3339)))
return vs
}
if consumerPeerID != "" && len(ae.AllowedPeerIDs) > 0 {
if !slices.Contains(ae.AllowedPeerIDs, consumerPeerID) {
add(AEViolationPeerNotAllowed, fmt.Sprintf(
"peer %s not allowed to use resource %s (AE %s)", consumerPeerID, resourceID, ae.ID))
}
}
if workflowID != "" && len(ae.AllowedWorkflowIDs) > 0 {
if !slices.Contains(ae.AllowedWorkflowIDs, workflowID) {
add(AEViolationWorkflowNotAllow, fmt.Sprintf(
"workflow %s not in allowed-workflow list for resource %s (AE %s)", workflowID, resourceID, ae.ID))
}
}
if ae.Coupling != nil {
for _, req := range ae.Coupling.RequiredResourceIDs {
if _, ok := workflowResourceIDs[req]; !ok {
add(AEViolationCouplingRequired, fmt.Sprintf(
"resource %s requires %s to be present (AE %s)", resourceID, req, ae.ID))
}
}
for _, forb := range ae.Coupling.ForbiddenResourceIDs {
if _, ok := workflowResourceIDs[forb]; ok {
add(AEViolationCouplingForbid, fmt.Sprintf(
"resource %s forbids co-use with %s (AE %s)", resourceID, forb, ae.ID))
}
}
}
return vs
}
// ── Violation types ───────────────────────────────────────────────────────────
type AEViolationType string
const (
AEViolationRevoked AEViolationType = "ae_revoked"
AEViolationExpired AEViolationType = "ae_expired"
AEViolationNotYetValid AEViolationType = "ae_not_yet_valid"
AEViolationPeerNotAllowed AEViolationType = "ae_peer_not_allowed"
AEViolationWorkflowNotAllow AEViolationType = "ae_workflow_not_allowed"
AEViolationCouplingRequired AEViolationType = "ae_coupling_required"
AEViolationCouplingForbid AEViolationType = "ae_coupling_forbidden"
)
// AEViolation describes a single constraint that was not satisfied.
type AEViolation struct {
AEID string
ResourceID string
Type AEViolationType
Message string
}
// ── NATS emit helper (uses tools only — no oclib circular import) ─────────────
// EmitAEBehaviorReport emits a PEER_BEHAVIOR_EVENT(BehaviorFraud) for each
// unique AE violation. Call this before rejecting the execution.
func EmitAEBehaviorReport(consumerPeerID string, violations []AEViolation) {
if consumerPeerID == "" || len(violations) == 0 {
return
}
seen := map[string]struct{}{}
for _, v := range violations {
key := v.AEID + ":" + v.ResourceID
if _, dup := seen[key]; dup {
continue
}
seen[key] = struct{}{}
report := tools.PeerBehaviorReport{
ReporterApp: "oc-scheduler",
TargetPeerID: consumerPeerID,
Severity: tools.BehaviorFraud,
Reason: fmt.Sprintf("AE violation (%s): %s", v.Type, v.Message),
Evidence: v.AEID,
At: time.Now().UTC(),
}
if b, err := json.Marshal(report); err == nil {
tools.NewNATSCaller().SetNATSPub(tools.PEER_BEHAVIOR_EVENT, tools.NATSResponse{
FromApp: "oc-scheduler",
Method: int(tools.PEER_BEHAVIOR_EVENT),
Payload: b,
})
}
}
}
// OriginType qualifies where a resource instance comes from.
type OriginType int
const (
// OriginPeer: instance offered by a known network peer (default).
OriginPeer OriginType = iota
// OriginPublic: instance from a public registry (Docker Hub, HuggingFace, etc.).
// No peer confirmation is needed; access is unrestricted.
OriginPublic
// OriginSelf: self-hosted instance with no third-party peer.
OriginSelf
)
// OriginMeta carries provenance information for a resource instance.
type OriginMeta struct {
Type OriginType `json:"origin_type" bson:"origin_type"`
Ref string `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` // e.g. "docker.io/pytorch/pytorch:2.1"
Verified bool `json:"origin_verified" bson:"origin_verified"` // manually vetted by an OC admin
}
// IsPeerless MUST NOT be used for authorization decisions.
// Use ResourceInstance.IsPeerless() instead, which derives the property
// from structural invariants rather than this self-declared field.
//
// This method is kept only for display/logging purposes.
func (o OriginMeta) DeclaredPeerless() bool {
return o.Type != OriginPeer
}