diff --git a/cmd/frontend/graphqlbackend/org.go b/cmd/frontend/graphqlbackend/org.go index ef5735b42b262..3625f85319982 100644 --- a/cmd/frontend/graphqlbackend/org.go +++ b/cmd/frontend/graphqlbackend/org.go @@ -54,9 +54,9 @@ type OrgResolver struct { func NewOrg(org *types.Org) *OrgResolver { return &OrgResolver{org: org} } -func (o *OrgResolver) ID() graphql.ID { return marshalOrgID(o.org.ID) } +func (o *OrgResolver) ID() graphql.ID { return MarshalOrgID(o.org.ID) } -func marshalOrgID(id int32) graphql.ID { return relay.MarshalID("Org", id) } +func MarshalOrgID(id int32) graphql.ID { return relay.MarshalID("Org", id) } func UnmarshalOrgID(id graphql.ID) (orgID int32, err error) { err = relay.UnmarshalSpec(id, &orgID) diff --git a/cmd/frontend/graphqlbackend/saved_searches.go b/cmd/frontend/graphqlbackend/saved_searches.go index a1d638bb098e9..e1af2b2d875a6 100644 --- a/cmd/frontend/graphqlbackend/saved_searches.go +++ b/cmd/frontend/graphqlbackend/saved_searches.go @@ -82,7 +82,7 @@ func (r savedSearchResolver) Query() string { return r.s.Query } func (r savedSearchResolver) Namespace(ctx context.Context) (*NamespaceResolver, error) { if r.s.OrgID != nil { - n, err := NamespaceByID(ctx, marshalOrgID(*r.s.OrgID)) + n, err := NamespaceByID(ctx, MarshalOrgID(*r.s.OrgID)) if err != nil { return nil, err } diff --git a/enterprise/internal/campaigns/resolvers/resolver.go b/enterprise/internal/campaigns/resolvers/resolver.go index 51e68548198c9..dbd13657f2cf5 100644 --- a/enterprise/internal/campaigns/resolvers/resolver.go +++ b/enterprise/internal/campaigns/resolvers/resolver.go @@ -175,7 +175,7 @@ func (r *Resolver) ChangesetSpecByID(ctx context.Context, id graphql.ID) (graphq func (r *Resolver) CreateCampaign(ctx context.Context, args *graphqlbackend.CreateCampaignArgs) (graphqlbackend.CampaignResolver, error) { var err error - tr, ctx := trace.New(ctx, "Resolver.CreateCampaign", fmt.Sprintf("CampaignSpec %s", args.CampaignSpec)) + tr, _ := trace.New(ctx, "Resolver.CreateCampaign", fmt.Sprintf("CampaignSpec %s", args.CampaignSpec)) defer func() { tr.SetError(err) tr.Finish() @@ -194,14 +194,9 @@ func (r *Resolver) ApplyCampaign(ctx context.Context, args *graphqlbackend.Apply tr.Finish() }() - user, err := db.Users.GetByCurrentAuthUser(ctx) - if err != nil { - return nil, errors.Wrapf(err, "%v", backend.ErrNotAuthenticated) - } - - // 🚨 SECURITY: Only site admins may create a campaign for now. - if !user.SiteAdmin { - return nil, backend.ErrMustBeSiteAdmin + // 🚨 SECURITY: Only site admins may apply campaigns for now. + if err := backend.CheckCurrentUserIsSiteAdmin(ctx); err != nil { + return nil, err } opts := ee.ApplyCampaignOpts{} @@ -211,6 +206,10 @@ func (r *Resolver) ApplyCampaign(ctx context.Context, args *graphqlbackend.Apply return nil, err } + if opts.CampaignSpecRandID == "" { + return nil, ErrIDIsZero + } + if args.EnsureCampaign != nil { opts.EnsureCampaignID, err = campaigns.UnmarshalCampaignID(*args.EnsureCampaign) if err != nil { @@ -324,7 +323,52 @@ func (r *Resolver) CreateChangesetSpec(ctx context.Context, args *graphqlbackend } func (r *Resolver) MoveCampaign(ctx context.Context, args *graphqlbackend.MoveCampaignArgs) (graphqlbackend.CampaignResolver, error) { - return nil, errors.New("TODO: not implemented") + var err error + tr, ctx := trace.New(ctx, "Resolver.MoveCampaign", fmt.Sprintf("Campaign %s", args.Campaign)) + defer func() { + tr.SetError(err) + tr.Finish() + }() + + campaignID, err := campaigns.UnmarshalCampaignID(args.Campaign) + if err != nil { + return nil, err + } + + if campaignID == 0 { + return nil, ErrIDIsZero + } + + var opts ee.MoveCampaignOpts + + if args.NewName != nil { + opts.NewName = *args.NewName + } + + if args.NewNamespace != nil { + newNamespace := *args.NewNamespace + switch relay.UnmarshalKind(newNamespace) { + case "User": + err = relay.UnmarshalSpec(newNamespace, &opts.NewNamespaceUserID) + case "Org": + err = relay.UnmarshalSpec(newNamespace, &opts.NewNamespaceOrgID) + default: + err = errors.Errorf("Invalid namespace %q", newNamespace) + } + + if err != nil { + return nil, err + } + } + + svc := ee.NewService(r.store, r.httpFactory) + // 🚨 SECURITY: MoveCampaign checks whether the current user is authorized. + campaign, err := svc.MoveCampaign(ctx, opts) + if err != nil { + return nil, err + } + + return &campaignResolver{store: r.store, httpFactory: r.httpFactory, Campaign: campaign}, nil } func (r *Resolver) DeleteCampaign(ctx context.Context, args *graphqlbackend.DeleteCampaignArgs) (_ *graphqlbackend.EmptyResponse, err error) { diff --git a/enterprise/internal/campaigns/resolvers/resolver_test.go b/enterprise/internal/campaigns/resolvers/resolver_test.go index 506c664c890f1..919a3c70df10d 100644 --- a/enterprise/internal/campaigns/resolvers/resolver_test.go +++ b/enterprise/internal/campaigns/resolvers/resolver_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/graph-gophers/graphql-go" "github.com/sourcegraph/sourcegraph/cmd/frontend/backend" + "github.com/sourcegraph/sourcegraph/cmd/frontend/db" "github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend" "github.com/sourcegraph/sourcegraph/cmd/repo-updater/repos" ee "github.com/sourcegraph/sourcegraph/enterprise/internal/campaigns" @@ -806,6 +807,7 @@ func TestNullIDResilience(t *testing.T) { campaigns.MarshalCampaignID(0), marshalExternalChangesetID(0), marshalCampaignSpecRandID(""), + marshalChangesetSpecRandID(""), } for _, id := range ids { @@ -823,6 +825,8 @@ func TestNullIDResilience(t *testing.T) { fmt.Sprintf(`mutation { closeCampaign(campaign: %q) { id } }`, campaigns.MarshalCampaignID(0)), fmt.Sprintf(`mutation { deleteCampaign(campaign: %q) { alwaysNil } }`, campaigns.MarshalCampaignID(0)), fmt.Sprintf(`mutation { syncChangeset(changeset: %q) { alwaysNil } }`, marshalExternalChangesetID(0)), + fmt.Sprintf(`mutation { applyCampaign(campaignSpec: %q) { id } }`, marshalCampaignSpecRandID("")), + fmt.Sprintf(`mutation { moveCampaign(campaign: %q, newName: "foobar") { id } }`, campaigns.MarshalCampaignID(0)), } for _, m := range mutations { @@ -1173,3 +1177,92 @@ mutation($campaignSpec: ID!, $ensureCampaign: ID){ } } ` + +func TestMoveCampaign(t *testing.T) { + if testing.Short() { + t.Skip() + } + + ctx := context.Background() + dbtesting.SetupGlobalTestDB(t) + + userID := insertTestUser(t, dbconn.Global, "move-campaign1", true) + + org, err := db.Orgs.Create(ctx, "org", nil) + if err != nil { + t.Fatal(err) + } + + store := ee.NewStore(dbconn.Global) + + campaignSpec := &campaigns.CampaignSpec{ + RawSpec: ct.TestRawCampaignSpec, + UserID: userID, + NamespaceUserID: userID, + } + if err := store.CreateCampaignSpec(ctx, campaignSpec); err != nil { + t.Fatal(err) + } + + campaign := &campaigns.Campaign{ + CampaignSpecID: campaignSpec.ID, + Name: "old-name", + AuthorID: userID, + NamespaceUserID: campaignSpec.UserID, + } + if err := store.CreateCampaign(ctx, campaign); err != nil { + t.Fatal(err) + } + + r := &Resolver{store: store} + s, err := graphqlbackend.NewSchema(r, nil, nil) + if err != nil { + t.Fatal(err) + } + + // Move to a new name + input := map[string]interface{}{ + "campaign": string(campaigns.MarshalCampaignID(campaign.ID)), + "newName": "new-name", + } + + var response struct{ MoveCampaign apitest.Campaign } + actorCtx := actor.WithActor(ctx, actor.FromUser(userID)) + apitest.MustExec(actorCtx, t, s, input, &response, mutationMoveCampaign) + + haveCampaign := response.MoveCampaign + fmt.Printf("haveCampaign=%+v\n", haveCampaign) + if diff := cmp.Diff(input["newName"], haveCampaign.Name); diff != "" { + t.Fatalf("unexpected name (-want +got):\n%s", diff) + } + + // Move to a new namespace + orgApiID := graphqlbackend.MarshalOrgID(org.ID) + input = map[string]interface{}{ + "campaign": string(campaigns.MarshalCampaignID(campaign.ID)), + "newNamespace": orgApiID, + } + + apitest.MustExec(actorCtx, t, s, input, &response, mutationMoveCampaign) + + haveCampaign = response.MoveCampaign + if diff := cmp.Diff(string(orgApiID), haveCampaign.Namespace.ID); diff != "" { + t.Fatalf("unexpected namespace (-want +got):\n%s", diff) + } +} + +const mutationMoveCampaign = ` +fragment u on User { id, databaseID, siteAdmin } +fragment o on Org { id, name } + +mutation($campaign: ID!, $newName: String, $newNamespace: ID){ + moveCampaign(campaign: $campaign, newName: $newName, newNamespace: $newNamespace) { + id, name, description, branch + author { ...u } + namespace { + ... on User { ...u } + ... on Org { ...o } + } + } +} +` diff --git a/enterprise/internal/campaigns/service.go b/enterprise/internal/campaigns/service.go index 7151df6bb2e51..df0f108023a5c 100644 --- a/enterprise/internal/campaigns/service.go +++ b/enterprise/internal/campaigns/service.go @@ -300,6 +300,60 @@ func (s *Service) ApplyCampaign(ctx context.Context, opts ApplyCampaignOpts) (ca return campaign, tx.UpdateCampaign(ctx, campaign) } +type MoveCampaignOpts struct { + CampaignID int64 + + NewName string + + NewNamespaceUserID int32 + NewNamespaceOrgID int32 +} + +func (o MoveCampaignOpts) String() string { + return fmt.Sprintf( + "CampaignID %d, NewName %q, NewNamespaceUserID %d, NewNamespaceOrgID %d", + o.CampaignID, + o.NewName, + o.NewNamespaceUserID, + o.NewNamespaceOrgID, + ) +} + +// MoveCampaign moves the campaign from one namespace to another and/or renames +// the campaign. +func (s *Service) MoveCampaign(ctx context.Context, opts MoveCampaignOpts) (campaign *campaigns.Campaign, err error) { + tr, ctx := trace.New(ctx, "Service.MoveCampaign", opts.String()) + defer func() { + tr.SetError(err) + tr.Finish() + }() + + tx, err := s.store.Transact(ctx) + if err != nil { + return nil, err + } + defer tx.Done(&err) + + campaign, err = tx.GetCampaign(ctx, GetCampaignOpts{ID: opts.CampaignID}) + if err != nil { + return nil, err + } + + if opts.NewName != "" { + campaign.Name = opts.NewName + } + + if opts.NewNamespaceOrgID != 0 { + campaign.NamespaceOrgID = opts.NewNamespaceOrgID + campaign.NamespaceUserID = 0 + } else if opts.NewNamespaceUserID != 0 { + campaign.NamespaceUserID = opts.NewNamespaceUserID + campaign.NamespaceOrgID = 0 + } + + return campaign, tx.UpdateCampaign(ctx, campaign) +} + // ErrEnsureCampaignFailed is returned by ApplyCampaign when a ensureCampaignID // is provided but a campaign with the name specified the campaignSpec exists // in the given namespace but has a different ID. diff --git a/enterprise/internal/campaigns/service_test.go b/enterprise/internal/campaigns/service_test.go index 205e790121a9e..66242d83683e7 100644 --- a/enterprise/internal/campaigns/service_test.go +++ b/enterprise/internal/campaigns/service_test.go @@ -850,6 +850,84 @@ func TestService(t *testing.T) { }) }) }) + + t.Run("MoveCampaign", func(t *testing.T) { + svc := NewServiceWithClock(store, cf, clock) + + createCampaign := func(t *testing.T, name string, authorID, userID, orgID int32) *campaigns.Campaign { + t.Helper() + + c := &campaigns.Campaign{ + AuthorID: authorID, + NamespaceUserID: userID, + NamespaceOrgID: orgID, + Name: name, + } + + if err := store.CreateCampaign(ctx, c); err != nil { + t.Fatal(err) + } + + return c + } + + t.Run("new name", func(t *testing.T) { + campaign := createCampaign(t, "old-name", user.ID, user.ID, 0) + + opts := MoveCampaignOpts{CampaignID: campaign.ID, NewName: "new-name"} + moved, err := svc.MoveCampaign(ctx, opts) + if err != nil { + t.Fatal(err) + } + + if have, want := moved.Name, opts.NewName; have != want { + t.Fatalf("wrong name. want=%q, have=%q", want, have) + } + }) + + t.Run("new user namespace", func(t *testing.T) { + campaign := createCampaign(t, "old-name", user.ID, user.ID, 0) + + user2 := createTestUser(ctx, t) + + opts := MoveCampaignOpts{CampaignID: campaign.ID, NewNamespaceUserID: user2.ID} + moved, err := svc.MoveCampaign(ctx, opts) + if err != nil { + t.Fatal(err) + } + + if have, want := moved.NamespaceUserID, opts.NewNamespaceUserID; have != want { + t.Fatalf("wrong NamespaceUserID. want=%d, have=%d", want, have) + } + + if have, want := moved.NamespaceOrgID, opts.NewNamespaceOrgID; have != want { + t.Fatalf("wrong NamespaceOrgID. want=%d, have=%d", want, have) + } + }) + + t.Run("new org namespace", func(t *testing.T) { + campaign := createCampaign(t, "old-name", user.ID, user.ID, 0) + + org, err := db.Orgs.Create(ctx, "org", nil) + if err != nil { + t.Fatal(err) + } + + opts := MoveCampaignOpts{CampaignID: campaign.ID, NewNamespaceOrgID: org.ID} + moved, err := svc.MoveCampaign(ctx, opts) + if err != nil { + t.Fatal(err) + } + + if have, want := moved.NamespaceUserID, opts.NewNamespaceUserID; have != want { + t.Fatalf("wrong NamespaceUserID. want=%d, have=%d", want, have) + } + + if have, want := moved.NamespaceOrgID, opts.NewNamespaceOrgID; have != want { + t.Fatalf("wrong NamespaceOrgID. want=%d, have=%d", want, have) + } + }) + }) } var testUser = db.NewUser{