Payment Flow + Access Flow Change

This commit is contained in:
mr
2026-05-27 15:50:23 +02:00
parent e6a9558cbf
commit cef23b5f30
40 changed files with 2227 additions and 410 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ func (abs *DataResource) ConvertToPricedResource(t tools.DataType, selectedInsta
type DataInstance struct {
ResourceInstance[*DataResourcePartnership]
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the data
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"`
}
func NewDataInstance(name string, peerID string) ResourceInstanceITF {
@@ -0,0 +1,223 @@
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
}
+7 -1
View File
@@ -38,7 +38,13 @@ func (d *NativeTool) ClearEnv() utils.DBObject {
}
func (w *NativeTool) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */
// WorkflowResource has no instances, but still carries AEs that must be
// filtered before the resource is returned to a non-owner, non-admin peer.
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
if request != nil {
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
return []ResourceInstanceITF{}
}
-31
View File
@@ -1,31 +0,0 @@
package resources
// 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"
License string `json:"origin_license,omitempty" bson:"origin_license,omitempty"` // SPDX identifier or free-form
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
}
+9 -9
View File
@@ -31,23 +31,23 @@ type ProcessingResource struct {
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"`
Usage *ProcessingUsage `bson:"usage,omitempty" json:"usage,omitempty"`
OpenSource bool `json:"open_source" bson:"open_source" default:"false"`
License string `json:"license,omitempty" bson:"license,omitempty"`
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
// IsService marks a long-running processing that acts as a persistent service.
// Such processings do not require a Compute booking (they manage their own lifecycle).
IsService bool `json:"is_service" bson:"is_service" default:"false"`
Maturity string `json:"maturity,omitempty" bson:"maturity,omitempty"`
// License is now in AbstractResource — kept here as alias for backward compat with existing DB docs.
// New code should use AbstractResource.License.
}
func (r *ProcessingResource) GetType() string {
return tools.PROCESSING_RESOURCE.String()
}
type ProcessingResourceAccess struct {
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
}
type ProcessingInstance struct {
ResourceInstance[*ResourcePartnerShip[*ProcessingResourcePricingProfile]]
Access *ProcessingResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
Access *ResourceAccess `json:"access,omitempty" bson:"access,omitempty"` // Access is the access
SizeGB int `json:"size_gb,omitempty" bson:"size_gb,omitempty"`
ContentType string `json:"content_type,omitempty" bson:"content_type,omitempty"`
}
func NewProcessingInstance(name string, peerID string) ResourceInstanceITF {
+51 -1
View File
@@ -39,6 +39,16 @@ type AbstractResource struct {
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
// License is the usage licence of the resource (SPDX identifier or free-text).
// Displayed prominently in the catalog detail view.
License string `json:"license,omitempty" bson:"license,omitempty"`
// ExploitationAuthorizations (AEs) are coupling and peer-usage constraints
// issued by the resource owner. Stored embedded in the resource document,
// NOT in a separate collection.
// Visibility-filtered per requesting peer before any response is sent.
ExploitationAuthorizations []ExploitationAuthorization `json:"exploitation_authorizations,omitempty" bson:"exploitation_authorizations,omitempty"`
}
func (ri *AbstractResource) Extend(typ ...string) map[string][]tools.DataType {
@@ -83,6 +93,28 @@ func (abs *AbstractResource) FilterPeer(peerID string) *dbs.Filters {
return nil
}
// GetExploitationAuthorizations returns all AEs attached to this resource.
// Used by oc-schedulerd's CheckWorkflowAE via structural interface assertion.
func (r *AbstractResource) GetExploitationAuthorizations() []ExploitationAuthorization {
return r.ExploitationAuthorizations
}
// FilterExploitationAuthorizations removes AEs that are not visible to peerID.
// Must be called before serializing the resource for a consumer peer.
// The resource owner (CreatorID) always sees all AEs unfiltered.
func (r *AbstractResource) FilterExploitationAuthorizations(peerID string, isAdmin bool) {
if isAdmin {
return // admin or owner: no filtering
}
filtered := r.ExploitationAuthorizations[:0]
for _, ae := range r.ExploitationAuthorizations {
if ae.IsVisibleTo(peerID, r.CreatorID) {
filtered = append(filtered, ae)
}
}
r.ExploitationAuthorizations = filtered
}
func (ri *AbstractResource) ClearEnv() utils.DBObject {
ri.Env = []models.Param{}
ri.Inputs = []models.Param{}
@@ -201,12 +233,15 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance(selected *int) Res
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest, instanceID ...string) []ResourceInstanceITF {
if !((request != nil && request.PeerID == abs.CreatorID && request.PeerID != "") || request.Admin) {
abs.Instances = VerifyAuthAction(abs.Instances, request, instanceID...)
// Filter AEs: only return AEs visible to the requesting peer.
if request != nil {
abs.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
inst := []ResourceInstanceITF{}
for _, i := range abs.Instances {
inst = append(inst, i)
}
return inst
}
@@ -528,3 +563,18 @@ func ToResource(
}
return nil, errors.New("can't found any data resources matching")
}
type ResourceAccess struct {
Source *models.PathSource `json:"source,omitempty" bson:"source,omitempty"`
Container *models.Container `json:"container,omitempty" bson:"container,omitempty"` // Container is the container
}
// HasSource returns true when the access is source-based (no embedded container).
func (a *ResourceAccess) HasSource() bool {
return a != nil && a.Container == nil && a.Source != nil
}
// HasContainer returns true when an explicit container image is provided.
func (a *ResourceAccess) HasContainer() bool {
return a != nil && a.Container != nil
}
+7 -1
View File
@@ -31,7 +31,13 @@ func (d *WorkflowResource) ClearEnv() utils.DBObject {
}
func (w *WorkflowResource) SetAllowedInstances(request *tools.APIRequest, ids ...string) []ResourceInstanceITF {
/* EMPTY */
// WorkflowResource has no instances, but still carries AEs that must be
// filtered before the resource is returned to a non-owner, non-admin peer.
if !((request != nil && request.PeerID == w.CreatorID && request.PeerID != "") || request.Admin) {
if request != nil {
w.FilterExploitationAuthorizations(request.PeerID, request.Admin)
}
}
return []ResourceInstanceITF{}
}