Payment Flow + Access Flow Change
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user