chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
package workflows
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
svc "github.com/portainer/portainer/api/gitops/workflows"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/set"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ErrNotFound indicates a workflow or one of its artifacts is not visible to the requesting user,
|
||||
// either because it does not exist or because access has been filtered out.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// ErrResourceNotAccessible indicates sc cannot access a resource that does exist. Callers
|
||||
// resolving a single artifact translate this to ErrNotFound to avoid leaking its existence.
|
||||
var ErrResourceNotAccessible = errors.New("resource not accessible")
|
||||
|
||||
// fetchWorkflowByID returns the detail view of a single workflow, resolving each of its
|
||||
// Artifacts to its backing Stack or EdgeStack and filtering out any the user cannot access.
|
||||
// A workflow with zero artifacts to begin with is a valid, visible state and is returned as-is.
|
||||
// ErrNotFound is returned when the workflow itself does not exist, or when it had artifacts but
|
||||
// every one of them was filtered out (existence-hiding for the common single-artifact case).
|
||||
func fetchWorkflowByID(
|
||||
tx dataservices.DataStoreTx,
|
||||
k8sFactory *cli.ClientFactory,
|
||||
sc *security.RestrictedRequestContext,
|
||||
workflowID portainer.WorkflowID,
|
||||
) (*WorkflowDetail, error) {
|
||||
wf, err := tx.Workflow().Read(workflowID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrNotFound, err)
|
||||
}
|
||||
|
||||
sourceMap := map[portainer.SourceID]portainer.Source{}
|
||||
if len(wf.Artifacts) > 0 {
|
||||
userContext := source.NewUserContext(sc.User, sc.UserMemberships)
|
||||
sourceMap, err = svc.LoadWorkflowSources(tx, userContext, wf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
artifacts := make([]svc.ArtifactDetail, 0, len(wf.Artifacts))
|
||||
for _, a := range wf.Artifacts {
|
||||
detail, err := fetchArtifactDetail(tx, k8sFactory, sc, a, sourceMap)
|
||||
if errors.Is(err, ErrNotFound) || errors.Is(err, ErrResourceNotAccessible) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
artifacts = append(artifacts, *detail)
|
||||
}
|
||||
|
||||
if len(wf.Artifacts) > 0 && len(artifacts) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
return &WorkflowDetail{
|
||||
ID: int(wf.ID),
|
||||
Name: wf.Name,
|
||||
Artifacts: artifacts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fetchArtifactDetail resolves a single Artifact to its backing Stack or EdgeStack.
|
||||
func fetchArtifactDetail(
|
||||
tx dataservices.DataStoreTx,
|
||||
k8sFactory *cli.ClientFactory,
|
||||
sc *security.RestrictedRequestContext,
|
||||
artifact portainer.Artifact,
|
||||
sourceMap map[portainer.SourceID]portainer.Source,
|
||||
) (*svc.ArtifactDetail, error) {
|
||||
switch {
|
||||
case artifact.StackID != 0:
|
||||
stack, err := tx.Stack().Read(artifact.StackID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fetchStackArtifact(tx, k8sFactory, sc, *stack, artifact, sourceMap)
|
||||
|
||||
case artifact.EdgeStackID != 0:
|
||||
if !sc.IsAdmin {
|
||||
return nil, ErrResourceNotAccessible
|
||||
}
|
||||
edgeStack, err := tx.EdgeStack().EdgeStack(artifact.EdgeStackID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fetchEdgeStackArtifact(tx, *edgeStack, artifact, sourceMap)
|
||||
|
||||
default:
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
// fetchStackArtifact resolves a stack-backed Artifact, applying the same endpoint/type-match and
|
||||
// access checks as FetchWorkflows, scoped to a single stack.
|
||||
func fetchStackArtifact(
|
||||
tx dataservices.DataStoreTx,
|
||||
k8sFactory *cli.ClientFactory,
|
||||
sc *security.RestrictedRequestContext,
|
||||
stack portainer.Stack,
|
||||
artifact portainer.Artifact,
|
||||
sourceMap map[portainer.SourceID]portainer.Source,
|
||||
) (*svc.ArtifactDetail, error) {
|
||||
endpointMap, err := svc.BuildEndpointMap(tx, []portainer.Stack{stack})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkStackAccessible(tx, k8sFactory, sc, stack, endpointMap, artifact, sourceMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sourcePhase, artifactPhase := svc.ArtifactPhases(artifact.Files, sourceMap)
|
||||
|
||||
detail := svc.MapStackToArtifactDetail(stack, artifact.Files, sourcePhase, artifactPhase)
|
||||
return &detail, nil
|
||||
}
|
||||
|
||||
// checkStackAccessible returns ErrResourceNotAccessible if sc cannot access stack, either because
|
||||
// its endpoint's Kubernetes namespace/source access is insufficient or because Docker's
|
||||
// resource-control-based UAC filters it out.
|
||||
func checkStackAccessible(
|
||||
tx dataservices.DataStoreTx,
|
||||
k8sFactory *cli.ClientFactory,
|
||||
sc *security.RestrictedRequestContext,
|
||||
stack portainer.Stack,
|
||||
endpointMap map[portainer.EndpointID]portainer.Endpoint,
|
||||
artifact portainer.Artifact,
|
||||
sourceMap map[portainer.SourceID]portainer.Source,
|
||||
) error {
|
||||
if stack.Type != portainer.KubernetesStack {
|
||||
accessible, err := svc.FilterDockerStacksByAccess(tx, []portainer.Stack{stack}, sc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(accessible) == 0 {
|
||||
return ErrResourceNotAccessible
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ep, epOk := endpointMap[stack.EndpointID]
|
||||
if !epOk {
|
||||
return ErrResourceNotAccessible
|
||||
}
|
||||
|
||||
access, err := svc.ResolveKubeAccess(k8sFactory, sc, &ep)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("context", "checkStackAccessible").Int("endpoint_id", int(ep.ID)).Msg("Failed to resolve kube access for endpoint, filtering artifact")
|
||||
return ErrResourceNotAccessible
|
||||
}
|
||||
|
||||
if (!access.IsKubeAdmin && !slices.Contains(access.NonAdminNamespaces, stack.Namespace)) || !HasAccessibleSource(artifact.Files, sourceMap) {
|
||||
return ErrResourceNotAccessible
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchEdgeStackArtifact resolves an edge-stack-backed Artifact. The caller (fetchArtifactDetail)
|
||||
// has already gated access via sc.IsAdmin, so edge stacks are visible to admins only.
|
||||
func fetchEdgeStackArtifact(
|
||||
tx dataservices.DataStoreTx,
|
||||
edgeStack portainer.EdgeStack,
|
||||
artifact portainer.Artifact,
|
||||
sourceMap map[portainer.SourceID]portainer.Source,
|
||||
) (*svc.ArtifactDetail, error) {
|
||||
statuses, err := tx.EdgeStackStatus().ReadAll(edgeStack.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupIDSet := set.ToSet(edgeStack.EdgeGroups)
|
||||
edgeGroups, err := tx.EdgeGroup().ReadAll(func(g portainer.EdgeGroup) bool {
|
||||
return groupIDSet.Contains(g.ID)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupEndpoints, err := svc.BuildGroupEndpoints(tx, edgeGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sourcePhase, artifactPhase := svc.ArtifactPhases(artifact.Files, sourceMap)
|
||||
|
||||
detail := svc.MapEdgeStackToArtifactDetail(edgeStack, artifact.Files, statuses, groupEndpoints, sourcePhase, artifactPhase)
|
||||
return &detail, nil
|
||||
}
|
||||
|
||||
// HasAccessibleSource reports whether any of the artifact's files has a source visible in
|
||||
// sourceMap.
|
||||
func HasAccessibleSource(files []portainer.ArtifactFile, sourceMap map[portainer.SourceID]portainer.Source) bool {
|
||||
return slices.ContainsFunc(files, func(f portainer.ArtifactFile) bool {
|
||||
_, ok := sourceMap[f.SourceID]
|
||||
return ok
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user