diff --git a/tavern/internal/ent/migrate/schema.go b/tavern/internal/ent/migrate/schema.go index 325a2dfaa..bfd89a375 100644 --- a/tavern/internal/ent/migrate/schema.go +++ b/tavern/internal/ent/migrate/schema.go @@ -31,7 +31,7 @@ var ( Symbol: "beacons_hosts_host", Columns: []*schema.Column{BeaconsColumns[9]}, RefColumns: []*schema.Column{HostsColumns[0]}, - OnDelete: schema.NoAction, + OnDelete: schema.Cascade, }, }, } @@ -100,7 +100,7 @@ var ( Symbol: "host_files_hosts_host", Columns: []*schema.Column{HostFilesColumns[11]}, RefColumns: []*schema.Column{HostsColumns[0]}, - OnDelete: schema.NoAction, + OnDelete: schema.Cascade, }, { Symbol: "host_files_tasks_reported_files", @@ -144,7 +144,7 @@ var ( Symbol: "host_processes_hosts_host", Columns: []*schema.Column{HostProcessesColumns[13]}, RefColumns: []*schema.Column{HostsColumns[0]}, - OnDelete: schema.NoAction, + OnDelete: schema.Cascade, }, { Symbol: "host_processes_tasks_reported_processes", @@ -233,7 +233,7 @@ var ( Symbol: "tasks_beacons_beacon", Columns: []*schema.Column{TasksColumns[10]}, RefColumns: []*schema.Column{BeaconsColumns[0]}, - OnDelete: schema.NoAction, + OnDelete: schema.Cascade, }, }, } diff --git a/tavern/internal/ent/schema/beacon.go b/tavern/internal/ent/schema/beacon.go index f58a98e6c..838897e0d 100644 --- a/tavern/internal/ent/schema/beacon.go +++ b/tavern/internal/ent/schema/beacon.go @@ -8,6 +8,7 @@ import ( "entgo.io/contrib/entgql" "entgo.io/ent" + "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" @@ -74,6 +75,9 @@ func (Beacon) Edges() []ent.Edge { edge.To("host", Host.Type). Required(). Unique(). + Annotations( + entsql.OnDelete(entsql.Cascade), + ). Comment("Host this beacon is running on."), edge.From("tasks", Task.Type). Annotations( diff --git a/tavern/internal/ent/schema/host_file.go b/tavern/internal/ent/schema/host_file.go index 6bd040cc6..d627ba268 100644 --- a/tavern/internal/ent/schema/host_file.go +++ b/tavern/internal/ent/schema/host_file.go @@ -9,6 +9,7 @@ import ( "entgo.io/contrib/entgql" "entgo.io/ent" + "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" @@ -63,11 +64,17 @@ func (HostFile) Edges() []ent.Edge { edge.To("host", Host.Type). Required(). Unique(). + Annotations( + entsql.OnDelete(entsql.Cascade), + ). Comment("Host the file was reported on."), edge.From("task", Task.Type). Required(). Unique(). Ref("reported_files"). + Annotations( + entsql.OnDelete(entsql.Cascade), + ). Comment("Task that reported this file."), } } diff --git a/tavern/internal/ent/schema/host_process.go b/tavern/internal/ent/schema/host_process.go index 5568d85e4..080d53f6c 100644 --- a/tavern/internal/ent/schema/host_process.go +++ b/tavern/internal/ent/schema/host_process.go @@ -3,6 +3,7 @@ package schema import ( "entgo.io/contrib/entgql" "entgo.io/ent" + "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" @@ -61,11 +62,17 @@ func (HostProcess) Edges() []ent.Edge { edge.To("host", Host.Type). Required(). Unique(). + Annotations( + entsql.OnDelete(entsql.Cascade), + ). Comment("Host the process was reported on."), edge.From("task", Task.Type). Required(). Unique(). Ref("reported_processes"). + Annotations( + entsql.OnDelete(entsql.Cascade), + ). Comment("Task that reported this process."), } } diff --git a/tavern/internal/ent/schema/task.go b/tavern/internal/ent/schema/task.go index c874f0c6b..eaa6c5644 100644 --- a/tavern/internal/ent/schema/task.go +++ b/tavern/internal/ent/schema/task.go @@ -6,6 +6,7 @@ import ( "entgo.io/contrib/entgql" "entgo.io/ent" + "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" @@ -59,9 +60,15 @@ func (Task) Edges() []ent.Edge { return []ent.Edge{ edge.From("quest", Quest.Type). Ref("tasks"). + Annotations( + entsql.OnDelete(entsql.Cascade), + ). Required(). Unique(), edge.To("beacon", Beacon.Type). + Annotations( + entsql.OnDelete(entsql.Cascade), + ). Required(). Unique(), edge.To("reported_files", HostFile.Type). diff --git a/tavern/internal/graphql/generated/mutation.generated.go b/tavern/internal/graphql/generated/mutation.generated.go index fc4605f72..45c37836d 100644 --- a/tavern/internal/graphql/generated/mutation.generated.go +++ b/tavern/internal/graphql/generated/mutation.generated.go @@ -17,6 +17,7 @@ import ( // region ************************** generated!.gotpl ************************** type MutationResolver interface { + DropAllData(ctx context.Context) (bool, error) CreateQuest(ctx context.Context, beaconIDs []int, input ent.CreateQuestInput) (*ent.Quest, error) UpdateBeacon(ctx context.Context, beaconID int, input ent.UpdateBeaconInput) (*ent.Beacon, error) UpdateHost(ctx context.Context, hostID int, input ent.UpdateHostInput) (*ent.Host, error) @@ -229,6 +230,74 @@ func (ec *executionContext) field_Mutation_updateUser_args(ctx context.Context, // region **************************** field.gotpl ***************************** +func (ec *executionContext) _Mutation_dropAllData(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_dropAllData(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DropAllData(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + role, err := ec.unmarshalNRole2realmᚗpubᚋtavernᚋinternalᚋgraphqlᚋmodelsᚐRole(ctx, "ADMIN") + if err != nil { + return nil, err + } + if ec.directives.RequireRole == nil { + return nil, errors.New("directive requireRole is not implemented") + } + return ec.directives.RequireRole(ctx, nil, directive0, role) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(bool); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be bool`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_dropAllData(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Mutation_createQuest(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createQuest(ctx, field) if err != nil { @@ -1121,6 +1190,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Mutation") + case "dropAllData": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_dropAllData(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createQuest": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createQuest(ctx, field) diff --git a/tavern/internal/graphql/generated/root_.generated.go b/tavern/internal/graphql/generated/root_.generated.go index ed29299e6..ecf5bd13e 100644 --- a/tavern/internal/graphql/generated/root_.generated.go +++ b/tavern/internal/graphql/generated/root_.generated.go @@ -119,6 +119,7 @@ type ComplexityRoot struct { CreateTag func(childComplexity int, input ent.CreateTagInput) int CreateTome func(childComplexity int, input ent.CreateTomeInput) int DeleteTome func(childComplexity int, tomeID int) int + DropAllData func(childComplexity int) int UpdateBeacon func(childComplexity int, beaconID int, input ent.UpdateBeaconInput) int UpdateHost func(childComplexity int, hostID int, input ent.UpdateHostInput) int UpdateTag func(childComplexity int, tagID int, input ent.UpdateTagInput) int @@ -670,6 +671,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteTome(childComplexity, args["tomeID"].(int)), true + case "Mutation.dropAllData": + if e.complexity.Mutation.DropAllData == nil { + break + } + + return e.complexity.Mutation.DropAllData(childComplexity), true + case "Mutation.updateBeacon": if e.complexity.Mutation.UpdateBeacon == nil { break @@ -2994,6 +3002,11 @@ scalar Uint64 } `, BuiltIn: false}, {Name: "../schema/mutation.graphql", Input: `type Mutation { + #### + # Admin + #### + dropAllData: Boolean! @requireRole(role: ADMIN) + ### # Quest ### diff --git a/tavern/internal/graphql/mutation.resolvers.go b/tavern/internal/graphql/mutation.resolvers.go index 031df1a58..704071ad5 100644 --- a/tavern/internal/graphql/mutation.resolvers.go +++ b/tavern/internal/graphql/mutation.resolvers.go @@ -14,6 +14,46 @@ import ( "realm.pub/tavern/internal/graphql/generated" ) +// DropAllData is the resolver for the dropAllData field. +func (r *mutationResolver) DropAllData(ctx context.Context) (bool, error) { + // Initialize Transaction + tx, err := r.client.Tx(ctx) + if err != nil { + return false, fmt.Errorf("failed to initialize transaction: %w", err) + } + client := tx.Client() + + // Delete relevant ents + if _, err := client.Beacon.Delete().Exec(ctx); err != nil { + return false, rollback(tx, fmt.Errorf("failed to delete beacons: %w", err)) + } + if _, err := client.HostFile.Delete().Exec(ctx); err != nil { + return false, rollback(tx, fmt.Errorf("failed to delete hostfiles: %w", err)) + } + if _, err := client.HostProcess.Delete().Exec(ctx); err != nil { + return false, rollback(tx, fmt.Errorf("failed to delete hostprocesses: %w", err)) + } + if _, err := client.Host.Delete().Exec(ctx); err != nil { + return false, rollback(tx, fmt.Errorf("failed to delete hosts: %w", err)) + } + if _, err := client.Quest.Delete().Exec(ctx); err != nil { + return false, rollback(tx, fmt.Errorf("failed to delete quests: %w", err)) + } + if _, err := client.Tag.Delete().Exec(ctx); err != nil { + return false, rollback(tx, fmt.Errorf("failed to delete tags: %w", err)) + } + if _, err := client.Task.Delete().Exec(ctx); err != nil { + return false, rollback(tx, fmt.Errorf("failed to delete tasks: %w", err)) + } + + // Commit + if err := tx.Commit(); err != nil { + return false, rollback(tx, fmt.Errorf("failed to commit transaction: %w", err)) + } + + return true, nil +} + // CreateQuest is the resolver for the createQuest field. func (r *mutationResolver) CreateQuest(ctx context.Context, beaconIDs []int, input ent.CreateQuestInput) (*ent.Quest, error) { // 1. Begin Transaction diff --git a/tavern/internal/graphql/schema.graphql b/tavern/internal/graphql/schema.graphql index dddd3d262..5e62246f3 100644 --- a/tavern/internal/graphql/schema.graphql +++ b/tavern/internal/graphql/schema.graphql @@ -1637,6 +1637,11 @@ input SubmitTaskResultInput { error: String } type Mutation { + #### + # Admin + #### + dropAllData: Boolean! @requireRole(role: ADMIN) + ### # Quest ### diff --git a/tavern/internal/graphql/schema/mutation.graphql b/tavern/internal/graphql/schema/mutation.graphql index adf5b1194..34cbc0a8e 100644 --- a/tavern/internal/graphql/schema/mutation.graphql +++ b/tavern/internal/graphql/schema/mutation.graphql @@ -1,4 +1,9 @@ type Mutation { + #### + # Admin + #### + dropAllData: Boolean! @requireRole(role: ADMIN) + ### # Quest ### diff --git a/tavern/internal/graphql/testdata/mutations/dropAllData/DropsData.yml b/tavern/internal/graphql/testdata/mutations/dropAllData/DropsData.yml new file mode 100644 index 000000000..b490db366 --- /dev/null +++ b/tavern/internal/graphql/testdata/mutations/dropAllData/DropsData.yml @@ -0,0 +1,26 @@ +state: | + INSERT INTO `users` (id,oauth_id,photo_url,name,session_token,access_token,is_activated,is_admin) + VALUES (5,"test_oauth_id","https://photos.com","test","secretToken","accessToken",true,true); + INSERT INTO `hosts` (id, name, identifier, platform, created_at, last_modified_at) + VALUES (1010,"db1","EXISTING-HOST", "PLATFORM_UNSPECIFIED", "2024-01-22 14:51:13", "2024-01-22 14:51:13"); + INSERT INTO `beacons` (id, name, identifier, beacon_host, created_at, last_modified_at) + VALUES (1337,"delightful-lich","ABCDEFG-123456",1010, "2024-01-22 14:51:13", "2024-01-22 14:51:13"); + INSERT INTO `tomes` (id, name, description, author, eldritch, hash, created_at, last_modified_at) + VALUES (2000,"Test Tome","Used in a unit test :D", "kcarretto", "print('Hello World!')", "abcdefg", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `files` (id, name, content, hash, created_at, last_modified_at) + VALUES (3000, "TestFile1", "hello world", "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `files` (id, name, content, hash, created_at, last_modified_at) + VALUES (3001, "TestFile2", "some test", "a9d9e8df0488c7e7e9236e43fe0c9385d7ea6920700db55d305f55dca76ddb0b", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `tome_files` (tome_id, file_id) + VALUES (2000, 3000); + INSERT INTO `tome_files` (tome_id, file_id) + VALUES (2000, 3001); +requestor: + session_token: secretToken +query: | + mutation DropAllData { + dropAllData + } + +expected: + dropAllData: true diff --git a/tavern/internal/graphql/testdata/mutations/dropAllData/PermissionDenied.yml b/tavern/internal/graphql/testdata/mutations/dropAllData/PermissionDenied.yml new file mode 100644 index 000000000..2bc7c7841 --- /dev/null +++ b/tavern/internal/graphql/testdata/mutations/dropAllData/PermissionDenied.yml @@ -0,0 +1,25 @@ +state: | + INSERT INTO `users` (id,oauth_id,photo_url,name,session_token,access_token,is_activated,is_admin) + VALUES (5,"test_oauth_id","https://photos.com","test","secretToken","accessToken",true,false); + INSERT INTO `hosts` (id, name, identifier, platform, created_at, last_modified_at) + VALUES (1010,"db1","EXISTING-HOST", "PLATFORM_UNSPECIFIED", "2024-01-22 14:51:13", "2024-01-22 14:51:13"); + INSERT INTO `beacons` (id, name, identifier, beacon_host, created_at, last_modified_at) + VALUES (1337,"delightful-lich","ABCDEFG-123456",1010, "2024-01-22 14:51:13", "2024-01-22 14:51:13"); + INSERT INTO `tomes` (id, name, description, author, eldritch, hash, created_at, last_modified_at) + VALUES (2000,"Test Tome","Used in a unit test :D", "kcarretto", "print('Hello World!')", "abcdefg", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `files` (id, name, content, hash, created_at, last_modified_at) + VALUES (3000, "TestFile1", "hello world", "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `files` (id, name, content, hash, created_at, last_modified_at) + VALUES (3001, "TestFile2", "some test", "a9d9e8df0488c7e7e9236e43fe0c9385d7ea6920700db55d305f55dca76ddb0b", "2023-03-04 14:51:13", "2023-03-04 14:51:13"); + INSERT INTO `tome_files` (tome_id, file_id) + VALUES (2000, 3000); + INSERT INTO `tome_files` (tome_id, file_id) + VALUES (2000, 3001); +requestor: + session_token: secretToken +query: | + mutation DropAllData { + dropAllData + } + +expected_error: "permission denied" diff --git a/tavern/internal/www/schema.graphql b/tavern/internal/www/schema.graphql index dddd3d262..5e62246f3 100644 --- a/tavern/internal/www/schema.graphql +++ b/tavern/internal/www/schema.graphql @@ -1637,6 +1637,11 @@ input SubmitTaskResultInput { error: String } type Mutation { + #### + # Admin + #### + dropAllData: Boolean! @requireRole(role: ADMIN) + ### # Quest ###