Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283package spindle
import ( "context" "strings" "sync" "testing" "time"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/api/org_tangled" "tangled.org/core/spindle/models" "tangled.org/core/spindle/pipeline" "tangled.org/core/spindle/secrets" "tangled.org/core/workflow")
const trustWorkflow = "engine: dummy\naudience: " + oidcTestAudience + "\nwhen:\n - event: pull_request\n branch: main\nsteps:\n - name: hi\n command: echo hi\n"
type runGrant struct { secrets []string caches int ref string trustedSource bool}
func (g runGrant) trusted() bool { return len(g.secrets) > 0 || g.caches > 0}
func (g runGrant) has(key string) bool { for _, k := range g.secrets { if k == key { return true } } return false}
type grantEngine struct { mu sync.Mutex grant runGrant ran bool}
func (e *grantEngine) InitWorkflow(twf workflow.WorkflowSpec, _ workflow.PipelineSpec) (*models.Workflow, error) { return &models.Workflow{ Name: twf.Name, Steps: []models.Step{recordingStep{}}, Caches: []models.CacheEntry{{Key: "deps", Paths: []string{"node_modules"}}}, }, nil}
func (e *grantEngine) SetupWorkflow(context.Context, models.WorkflowId, *models.Workflow, models.WorkflowLogger) error { return nil}
func (e *grantEngine) WorkflowTimeout() time.Duration { return time.Minute }
func (e *grantEngine) DestroyWorkflow(context.Context, models.WorkflowId) error { return nil }
func (e *grantEngine) RunStep(_ context.Context, _ models.WorkflowId, w *models.Workflow, _ int, unlocked []secrets.UnlockedSecret, _ models.WorkflowLogger) error { e.mu.Lock() defer e.mu.Unlock() e.ran = true e.grant = runGrant{caches: len(w.Caches), ref: w.Environment["TANGLED_REF"], trustedSource: w.TrustedSource} for _, s := range unlocked { e.grant.secrets = append(e.grant.secrets, s.Key) } return nil}
type trustEnv struct { *prPipelineEnv eng *grantEngine}
func newTrustEnv(t *testing.T) *trustEnv { t.Helper() e := newPRPipelineEnv(t) addTestSecret(t, e.n.s, testRepoDid) e.n.s.oidc = oidcTestIssuer(t) eng := &grantEngine{} e.n.s.engs = map[string]models.Engine{"dummy": eng} return &trustEnv{prPipelineEnv: e, eng: eng}}
func (e *trustEnv) head(t *testing.T) string { t.Helper() return e.repo.commitWorkflows(t, map[string]string{"pr.yaml": trustWorkflow})}
// a patch whose record names where its head came from, the way submitPatch writes itfunc (e *trustEnv) patchFrom(rkey, ticket, head string, editor, source syntax.DID, branch string) { record := e.putPatch(testRepoDid, rkey, ticket, head, editor) record["source"] = map[string]any{"repo": source.String(), "branch": branch} e.knot.putVersion(testRepoDid, org_tangled.ReviewPatchNSID, rkey, editor, record) e.n.consumed(e.emitPatch(testRepoDid, rkey, opCreate))}
// runs the pipeline's queued job the way a job worker wouldfunc (e *trustEnv) run(t *testing.T, id models.PipelineId) runGrant { t.Helper() ctx := context.Background() for { job, err := e.n.s.db.DequeueJob(ctx, nil) if err != nil { t.Fatal(err) } if job == nil { t.Fatalf("pipeline %s has no queued job", id) } if job.PipelineId != id { continue } e.eng.mu.Lock() e.eng.ran, e.eng.grant = false, runGrant{} e.eng.mu.Unlock() e.n.s.runJob(ctx, job) e.eng.mu.Lock() defer e.eng.mu.Unlock() if !e.eng.ran { t.Fatalf("pipeline %s never ran a step", id) } return e.eng.grant }}
func (e *trustEnv) approve(t *testing.T, id models.PipelineId) { t.Helper() if err := e.n.s.ApprovePipeline(context.Background(), testRepoDid, id); err != nil { t.Fatal(err) }}
func (e *trustEnv) retry(t *testing.T, id models.PipelineId) models.PipelineId { t.Helper() retried, err := e.n.s.RetryPipeline(context.Background(), testOwner, testRepoDid, id, nil) if err != nil { t.Fatal(err) } return retried}
func wantUntrusted(t *testing.T, what string, got runGrant) { t.Helper() if got.trusted() || got.trustedSource || got.has(models.IDTokenEnvVar) { t.Errorf("%s ran with secrets %v, %d caches and TrustedSource %t, want none: its code isn't the repo's", what, got.secrets, got.caches, got.trustedSource) }}
func wantTrusted(t *testing.T, what string, got runGrant) { t.Helper() if !got.has("api_key") || !got.has(models.IDTokenEnvVar) || got.caches == 0 || !got.trustedSource { t.Errorf("%s ran with secrets %v, %d caches and TrustedSource %t, want the repo secret, an id token, its caches and TrustedSource", what, got.secrets, got.caches, got.trustedSource) }}
func TestForkPullRunsUntrusted(t *testing.T) { e := newTrustEnv(t) e.ticketAs(otherPull, "open", testDave, opCreate) e.patchFrom("bafydaveone", otherPull, e.head(t), testDave, faultOtherRepoDid, "feature") first := expectPRPipelines(t, e.n, 1)[0] wantBlocked(t, e.n, first, true)
e.approve(t, first.ID) wantUntrusted(t, "an approved fork pull", e.run(t, first.ID)) wantUntrusted(t, "the retry of an approved fork pull", e.run(t, e.retry(t, first.ID)))
e.patchFrom("bafydavetwo", otherPull, e.head(t), testDave, faultOtherRepoDid, "feature") later := expectPRPipelines(t, e.n, 3)[2] wantBlocked(t, e.n, later, true) e.approve(t, later.ID) wantUntrusted(t, "a later round of an approved fork pull", e.run(t, later.ID))
// a collaborator's fork needs no approval, and still isn't the repo's code e.ticketAs(pullRkey, "open", testBob, opCreate) e.patchFrom(patchOne, pullRkey, e.head(t), testBob, faultOtherRepoDid, "feature") own := expectPRPipelines(t, e.n, 4)[3] wantBlocked(t, e.n, own, false) wantUntrusted(t, "a collaborator's fork pull", e.run(t, own.ID))}
func TestSameRepoBranchPullRunsTrusted(t *testing.T) { e := newTrustEnv(t) e.ticketAs(pullRkey, "open", testBob, opCreate) e.patchFrom(patchOne, pullRkey, e.head(t), testBob, testRepoDid, "feature") got := expectPRPipelines(t, e.n, 1)[0] wantBlocked(t, e.n, got, false)
wantTrusted(t, "a collaborator's branch of the repo", e.run(t, got.ID)) wantTrusted(t, "the retry of a collaborator's branch", e.run(t, e.retry(t, got.ID)))}
// a patch record says what the client claims. nothing checks the branch against the headfunc TestClaimedBranchOfANonCollaboratorRunsUntrusted(t *testing.T) { e := newTrustEnv(t) e.ticketAs(otherPull, "open", testDave, opCreate) e.patchFrom("bafydaveone", otherPull, e.head(t), testDave, testRepoDid, "main") got := expectPRPipelines(t, e.n, 1)[0] wantBlocked(t, e.n, got, true)
e.approve(t, got.ID) run := e.run(t, got.ID) wantUntrusted(t, "an approved pull claiming main", run) if run.ref == "refs/heads/main" { t.Errorf("an approved pull claiming main ran as %s, want it scoped to the pull", run.ref) } wantUntrusted(t, "its retry", e.run(t, e.retry(t, got.ID)))}
func (e *trustEnv) enqueue(t *testing.T, id models.PipelineId, trigger workflow.TriggerMetadata, source *workflow.TriggerRepo, blocked bool) models.PipelineId { t.Helper() audience := oidcTestAudience tpl := workflow.PipelineSpec{ TriggerMetadata: &trigger, Workflows: []*workflow.WorkflowSpec{{Name: "pr.yaml", Engine: "dummy", Raw: trustWorkflow, Audience: &audience}}, } record, err := pipeline.FromTangled(id, time.Now(), tpl) if err != nil { t.Fatal(err) } if err := e.n.s.processPipeline(context.Background(), testRepoDid, tpl, record, source, blocked); err != nil { t.Fatal(err) } return id}
func (e *trustEnv) triggerRepo() *workflow.TriggerRepo { repoDid := testRepoDid.String() return &workflow.TriggerRepo{Did: testOwner.String(), Knot: "knot.test", RepoDid: &repoDid}}
// a stored record still carries whatever branch the patch claimedfunc TestStoredPullRunsStayUntrusted(t *testing.T) { e := newTrustEnv(t) dave, action := testDave.String(), "opened" pull := "at://" + testRepoDid.String() + "/" + org_tangled.TrackTicketNSID + "/" + otherPull stored := func(id models.PipelineId, branch string, blocked bool) models.PipelineId { return e.enqueue(t, id, workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPullRequest), Actor: &dave, Repo: e.triggerRepo(), PullRequest: &workflow.PullRequestTriggerData{ Action: &action, Pull: &pull, SourceBranch: branch, SourceSha: e.head(t), TargetBranch: "main", }, }, e.triggerRepo(), blocked) }
claimed := stored("3mstoredclaim", "main", true) e.approve(t, claimed) wantUntrusted(t, "a stored pull claiming main, approved", e.run(t, claimed)) wantUntrusted(t, "its retry", e.run(t, e.retry(t, claimed)))
fork := stored("3mstoredfork2", "", false) wantUntrusted(t, "a stored fork pull", e.run(t, fork)) wantUntrusted(t, "its retry", e.run(t, e.retry(t, fork)))}
func TestPushAndManualRunsKeepTheirTrust(t *testing.T) { e := newTrustEnv(t) owner, sha, ref := testOwner.String(), e.head(t), "refs/heads/main"
push := e.enqueue(t, "3mtrustpush22", workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindPush), Actor: &owner, Repo: e.triggerRepo(), Push: &workflow.PushTriggerData{Ref: ref, NewSha: sha, OldSha: strings.Repeat("0", 40)}, }, e.triggerRepo(), false) wantTrusted(t, "a push", e.run(t, push))
fork := faultOtherRepoDid.String() manual := e.enqueue(t, "3mtrustmanual", workflow.TriggerMetadata{ Kind: string(workflow.TriggerKindManual), Actor: &owner, Repo: e.triggerRepo(), Manual: &workflow.ManualTriggerData{Sha: sha, Ref: &ref}, SourceRepo: &fork, }, &workflow.TriggerRepo{RepoDid: &fork, Did: fork, Knot: "knot.test"}, false) wantUntrusted(t, "a manual run of a fork", e.run(t, manual))}