From d6a1b9f92c534e7f6c1960871e52fe25a3aa38d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=2E=20Requena=20Plens?= Date: Sat, 19 Sep 2026 22:41:10 +0200 Subject: [PATCH] Name the package each admin action routes to as its owner `adminspecs` declared itself the owner of all ninety-two instance administration actions, and it is the one package among them that never issues a request: every handler those actions route to lives in one of the twenty-three domain packages the route names, and that is the package the test transport writes into the request inventory. The owner is a join key rather than a label. R-PATH's observation check joins the catalog to the inventory on that name and on nothing else, so ninety-two actions could be joined to no recording at all, and a declaration in `declarations.go` existed only to keep the gate quiet about it. `adminOptions` now takes the owner beside the route, each one a constant naming the package whose handler that route calls, and the declaration is retired. The table is left in place and empty, which is the healthy state for it rather than an unfinished one. The owner constants are not trusted on their own. `TestActionSpecs_OwnerPackage_IsThePackageTheHandlerComesFrom` holds each of the ninety-two against the package its route's input type comes from, which is a fact of the route rather than a second copy of the table: every admin handler takes the input type its own package declares, so a copy-pasted constant fails instead of landing. That is the one error R-PATH itself cannot catch, since an owner naming a real package and the wrong one reads exactly like a right one. R-PATH now observes 1082 of 1082 catalog actions, with no silent package and no declaration. The sharper join immediately earned its keep in R-PAGE, which reports rather than gates: `admin.system_hook_list` moved out of the not-asked-about count and into the findings, because `systemhooks` records exactly one paginated endpoint, `GET /hooks`, which is the endpoint that action calls. It answers with a bare array of hooks and accepts no `page` or `per_page`, which is the `user.list_impersonation_tokens` class R-PAGE was built for and which the coarse owner had been hiding. Left as a finding, deliberately: there is no declaration category for a list GitLab pages and we do not. (cherry picked from commit 2a44dd1214acae9cf3ee0db0c5419751dab4ff22) --- CLAUDE.md | 4 +- cmd/audit_1to1/internal/paths/declarations.go | 18 +- cmd/audit_1to1/internal/paths/doc.go | 16 +- cmd/internal/requestinventory/coverage.go | 12 +- internal/tools/adminspecs/action_specs.go | 254 ++++++++++-------- .../tools/adminspecs/action_specs_test.go | 36 ++- 6 files changed, 210 insertions(+), 130 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fa659451c..8186d1b66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -327,7 +327,7 @@ The sixth dimension of the 1:1 audit, `go run ./cmd/audit_1to1/ -scope=paths`, a Five checks, three of which gate. Four need no network and no suite run (`make audit-1to1-paths`, run by `make analyze` and by CI's generated-artifacts job); the endpoint one needs 250 documentation pages, so it runs only when asked for (`make audit-1to1-paths-endpoints`) and nothing schedules it: -- **Has the path ever been observed.** An action whose owning package issued no request has never had its request seen by anything. Held at package grain, because nothing on the wire names an action, and the report says so beside the number (`actions_observed_grain`): 990 of 1082 means 990 actions whose _owner_ issued something, which is a regression guard and not per-action assurance. A silent package means either that nothing drives it or that the inventory is stale, and the finding cannot tell those apart. A package silent for a reason is held to a declaration with a category and a reason in `cmd/audit_1to1/internal/paths/declarations.go`, and a declaration that no longer describes the tree is itself a finding. One entry today: `internal/tools/adminspecs` declares specs whose handlers live in other packages, so its 92 actions' requests are recorded under the packages that make them. An action whose owner names no package fails too: nothing in the catalog validates that field, and such an action used to be classified unmapped, which the gate ignored and `-gaps-only` dropped, so it could be neither counted nor seen. The owner `tools` is the catalog's own exception, for the orchestration package rather than a domain under it, and resolves to `internal/tools`. An owner that names a real package and the wrong one is a lie no version of this can catch, since the recording joins on that name and nothing else. +- **Has the path ever been observed.** An action whose owning package issued no request has never had its request seen by anything. Held at package grain, because nothing on the wire names an action, and the report says so beside the number (`actions_observed_grain`): 1082 of 1082 means 1082 actions whose _owner_ issued something, which is a regression guard and not per-action assurance. A silent package means either that nothing drives it or that the inventory is stale, and the finding cannot tell those apart. A package silent for a reason is held to a declaration with a category and a reason in `cmd/audit_1to1/internal/paths/declarations.go`, and a declaration that no longer describes the tree is itself a finding. No package is declared today: `internal/tools/adminspecs` was the one entry, for declaring 92 specs whose handlers live in other packages, until each of those actions was given the domain package its route names as its owner. Naming the owner where the handler lives is the better answer of the two, because a declaration excuses a join that cannot be made while a true owner makes it, and the sharper join is what put `admin.system_hook_list` on R-PAGE's list below. An action whose owner names no package fails too: nothing in the catalog validates that field, and such an action used to be classified unmapped, which the gate ignored and `-gaps-only` dropped, so it could be neither counted nor seen. The owner `tools` is the catalog's own exception, for the orchestration package rather than a domain under it, and resolves to `internal/tools`. An owner that names a real package and the wrong one is a lie no version of this can catch, since the recording joins on that name and nothing else. **The grain sharpens to the action where an end-to-end run speaks** (`e2e_observation`, `make audit-1to1-paths-e2e`). The unit recording cannot name an action because the httptest server answers on its own goroutine; a Docker run can, because the harness stamps a trace id into each MCP call, the server's span carries it back with the route the dispatcher actually chose, and every GitLab request the handler made is a child of that span. The receiver counts those children per trace and the harness writes the count on the `dispatch` line it already emits (`e2ecalls.Dispatch.Requests`), so `-e2e-calls ` answers, per action, which were seen issuing a request and which ran and issued none. It **reports and never gates**, and both halves of that are about what a missing record means: the shards are a byproduct of a run CI does not schedule and never commits, so failing on their absence would fail every push, and the counts are floors, since the batching span processor drops silently when its queue overflows. A positive claim is therefore solid and a negative one is a lead. A dispatch carrying a refusal reason is kept off the lead list, because the server declining to run something is not a handler that could not build a request. - **Does the document validate.** Every raw GraphQL document in the source against the pinned schema, through `cmd/internal/graphqldocs`, shared with `make check-graphql-documents`. It judges the document and never the values: four of the nine broken tools sent a document the schema refuses and the other five sent an accepted document carrying a value GitLab does not have, and the second half is caught by `internal/testutil`'s validating transport, which checks the variables with the document on every request a test drives. Neither substitutes for the other. @@ -350,7 +350,7 @@ Five checks, three of which gate. Four need no network and no suite run (`make a **It reports and does not gate**, for a different reason than the REST joins: the pin is exactly what GitLab serves, so the oracle is not incomplete, but a field GitLab offers is a candidate for the surface rather than a defect in it, GitLab adds fields weekly, and this dimension has **no tier oracle and no deprecation oracle** — the schema declares no tier (GitLab gates a GraphQL field at resolve time) and the pin carries no `@deprecated`, because `cmd/internal/graphqlintrospect` drops the directives and descriptions on decode. Both blind spots are stated on the report rather than silently absorbed, and both are closed by one sidecar record written from the introspection fetch that already carries the data. Nine of the eleven domains are Premium or Ultimate surfaces, so most of these findings sit on licensed ground: a `tier` is omitted from a finding rather than emitted empty, since an empty field would read as a condition that was checked and found absent. The one sub-class that **does** gate, and on every run, is a mutation payload whose `errors` **no field of the decoder reads**, which drops GitLab's account of a refused mutation and reports success; the condition is the decoder and not the document, because a payload that selects its errors and decodes none loses them just as completely, while the reverse is already a hard failure of the always-empty leg. A finding is answered by an entry in `cmd/audit_graphql_shapes/sent_declarations.go`, keyed by package, schema type and field, and a stale declaration fails the run on the terms every declaration table here is held to; two of the nine answer the work item a notes query names only to reach its notes widget, which is 68 rows. The record is deliberately uncommitted and not freshness-gated: a schema re-pin would churn it every time. -- **Does a list say where it ends.** R-PAGE, added because a blind spot was found by accident and every dimension above was green on it: `internal/tools/impersonationtokens` answers `user.list_impersonation_tokens` with a bare array of tokens while GitLab serves twenty at a time, so a caller cannot tell it has one page and cannot ask for the second. None of the six could see it, because all six compare a **published field** against the SDK struct, the documentation or the entity record, and GitLab's pagination is a field of no entity: an offset page arrives in the `X-Page`, `X-Next-Page`, `X-Per-Page`, `X-Total` and `X-Total-Pages` response headers, and a keyset page in a `Link`. The oracle is the same live record read through its params rather than its entities: 308 of its 2110 routes declare `per_page`, 304 with `page` beside it (offset) and 4 with a `cursor` or `page_token` instead (keyset, counted apart and raising no finding, since a page-and-total block is the wrong shape for one; none is reached by a recorded request today). An action is judged when its output is a **collection envelope** — exactly one content field, a list of objects, with this server's own framing taken out — which is where "the route declares the params but the action reads a single object" is answered structurally rather than one declaration at a time: a project carrying `shared_with_groups` is a single-object read, and admitting those turned 24 findings into 79. Of 268 collection-reading actions, 224 publish a pagination block, 20 sit in a package no paginated endpoint was recorded for, and **24 are findings**, 14 of them naming an action whose own route declares `per_page`. The join is the package, like the observation check's, and `pagination_endpoint_grain` says so beside the number; the 10 findings that matched a sibling's endpoint are declared in `cmd/audit_1to1/internal/paths/pagination_declarations.go` with the route the record holds for each (`GET /projects/:id/issues/:issue_iid/participants` declares neither param; `GET /projects/:id/languages` answers with an object rather than an array; `project.target_branch_rule_list` is read over GraphQL and has no REST route at all), and there is deliberately no category for "GitLab pages this and we have decided not to" — that is the finding. It **reports and does not gate**: a finding is a surface change, and `request_paginates` on each says whether the fix is one output field or an input that has to offer `page` and `per_page` first. Its declaration table gates like every other one here. +- **Does a list say where it ends.** R-PAGE, added because a blind spot was found by accident and every dimension above was green on it: `internal/tools/impersonationtokens` answers `user.list_impersonation_tokens` with a bare array of tokens while GitLab serves twenty at a time, so a caller cannot tell it has one page and cannot ask for the second. None of the six could see it, because all six compare a **published field** against the SDK struct, the documentation or the entity record, and GitLab's pagination is a field of no entity: an offset page arrives in the `X-Page`, `X-Next-Page`, `X-Per-Page`, `X-Total` and `X-Total-Pages` response headers, and a keyset page in a `Link`. The oracle is the same live record read through its params rather than its entities: 308 of its 2110 routes declare `per_page`, 304 with `page` beside it (offset) and 4 with a `cursor` or `page_token` instead (keyset, counted apart and raising no finding, since a page-and-total block is the wrong shape for one; none is reached by a recorded request today). An action is judged when its output is a **collection envelope** (exactly one content field, a list of objects, with this server's own framing taken out), which is where "the route declares the params but the action reads a single object" is answered structurally rather than one declaration at a time: a project carrying `shared_with_groups` is a single-object read, and admitting those turned 24 findings into 79. Of 268 collection-reading actions, 224 publish a pagination block, 19 sit in a package no paginated endpoint was recorded for, and **25 are findings**, 15 of them undeclared. `admin.system_hook_list` is the newest and shows what the join costs when an owner is coarse: it was in the not-asked-about count while its owner was `adminspecs`, which records nothing, and naming `systemhooks` as its owner put it against the one paginated endpoint that package records, `GET /hooks`, which is the endpoint the action itself calls. The join is the package, like the observation check's, and `pagination_endpoint_grain` says so beside the number; the 10 findings that matched a sibling's endpoint are declared in `cmd/audit_1to1/internal/paths/pagination_declarations.go` with the route the record holds for each (`GET /projects/:id/issues/:issue_iid/participants` declares neither param; `GET /projects/:id/languages` answers with an object rather than an array; `project.target_branch_rule_list` is read over GraphQL and has no REST route at all), and there is deliberately no category for "GitLab pages this and we have decided not to": that is the finding. It **reports and does not gate**: a finding is a surface change, and `request_paginates` on each says whether the fix is one output field or an input that has to offer `page` and `per_page` first. Its declaration table gates like every other one here. What none of it can do is check that GitLab answers a **specific request** the way our output struct expects: the shape check reads what GitLab's document says an endpoint returns, never what an instance returned for a call we made. The layers stack: the pinned schema and the inventory catch a request that cannot work, a real instance catches a response we misread, and the other five rules keep catching the surface we failed to expose. One more limit is worth knowing because it hid a defect: the inventory records which parameter names an endpoint was sent, never which of them were sent together, so a combination GitLab refuses is invisible here. `gitlab_get_catalog_resource` was in that state (`id` and `full_path` are both optional and GitLab accepts exactly one), and the answer to that class is a handler that refuses the combination and a schema that says so, not an inventory of every combination the fixtures happen to use. diff --git a/cmd/audit_1to1/internal/paths/declarations.go b/cmd/audit_1to1/internal/paths/declarations.go index 07f5a80e4..e5b0bd395 100644 --- a/cmd/audit_1to1/internal/paths/declarations.go +++ b/cmd/audit_1to1/internal/paths/declarations.go @@ -32,12 +32,12 @@ const ( // A declaration that no longer describes the tree is a finding too: a package // that has since recorded a request, or that no longer owns any action, leaves // a claim behind that a later reader would trust. -var declaredSilentOwners = map[string]silentOwnerDeclaration{ - "adminspecs": { - Category: categoryRecordedElsewhere, - Reason: "declares the instance-administration specs whose handlers live in the domain packages " + - "(topics, settings, system hooks and twenty more), so every request it owns an action for is " + - "recorded under the package that issues it. Its own silence says nothing about whether those " + - "requests were seen, which is why the count of silent actions is read package by package.", - }, -} +// +// The table is empty, and an empty one is the healthy state rather than an +// unfinished one. It held a single entry until then: internal/tools/adminspecs +// declared the 92 instance-administration actions while their handlers, and so +// their requests, live in the twenty-three domain packages the routes name. +// Each of those actions now names the package it routes to, which is the answer +// a declaration can only approximate, since a declaration excuses a join that +// cannot be made and a true owner makes it. +var declaredSilentOwners = map[string]silentOwnerDeclaration{} diff --git a/cmd/audit_1to1/internal/paths/doc.go b/cmd/audit_1to1/internal/paths/doc.go index a113c5cf1..430d93cb1 100644 --- a/cmd/audit_1to1/internal/paths/doc.go +++ b/cmd/audit_1to1/internal/paths/doc.go @@ -62,12 +62,16 @@ // What no version of this can catch is an owner that names a real package and // the wrong one, because the recording joins on that name and nothing else. // -// A package may nevertheless be silent for a reason, and internal/tools/adminspecs -// is the whole of it today: it declares specs whose handlers live in other -// packages, so its requests are recorded under the package that made them. -// Such a package is held to a declaration with a reason, the way -scope=sdk -// holds a client-go service to one, and a declaration that no longer describes -// the tree is itself a finding. +// A package may nevertheless be silent for a reason: it may declare specs +// whose handlers live in other packages, so that its requests are recorded +// under the packages that made them. Such a package is held to a declaration +// with a reason, the way -scope=sdk holds a client-go service to one, and a +// declaration that no longer describes the tree is itself a finding. No +// package is declared today. internal/tools/adminspecs was the one entry, for +// exactly that reason, until each of its 92 admin actions was given the domain +// package its route names as its owner; naming the owner where the handler +// lives is the better answer, because a declaration only excuses a join that +// cannot be made while an owner that is true makes it. // // # Does the document validate // diff --git a/cmd/internal/requestinventory/coverage.go b/cmd/internal/requestinventory/coverage.go index 22cd52ca5..3d94f16ba 100644 --- a/cmd/internal/requestinventory/coverage.go +++ b/cmd/internal/requestinventory/coverage.go @@ -89,11 +89,13 @@ type Owner struct { // catalog's ownership metadata rather than in any test. // // Silent is weaker than "never exercised" for a second reason beyond the -// coarse grain, and internal/tools/adminspecs is the whole of it today: a -// package may declare specs whose handlers live in other packages, and the -// request is then recorded under the package that made it while the count of -// silent actions blames the one that declared them. Reading the silent list as -// a work list means reading it package by package, not action by action. +// coarse grain: a package may declare specs whose handlers live in other +// packages, and the request is then recorded under the package that made it +// while the count of silent actions blames the one that declared them. +// internal/tools/adminspecs was that case for 92 actions until each of them was +// given the domain package its route names as its owner. Reading the silent +// list as a work list still means reading it package by package, not action by +// action. type Coverage struct { Total int Covered int diff --git a/internal/tools/adminspecs/action_specs.go b/internal/tools/adminspecs/action_specs.go index 77257bd67..0d39a9ac9 100644 --- a/internal/tools/adminspecs/action_specs.go +++ b/internal/tools/adminspecs/action_specs.go @@ -33,6 +33,45 @@ const ( actionAdminMetadataGet = actionMetadataGet ) +// Owner packages. This package declares the instance-administration specs and +// issues no request of its own: every admin handler lives in the domain +// package its route names, and that is the package the request recording +// writes down. The owner is therefore stated per action, beside the route it +// has to agree with, and never as one name for the whole file. +// +// It matters because the owner is a join key rather than a label. The request +// inventory names a package and nothing else, so an action owned by a package +// that issues nothing can be joined to no recording at all, and an action +// owned by the wrong package is attributed to somebody else's requests. +// TestActionSpecs_OwnerPackage_IsThePackageTheHandlerComesFrom holds each of +// these against the package its route's input type comes from, so a +// copy-pasted constant fails rather than lands. +const ( + ownerAlertManagement = "alertmanagement" + ownerAppStatistics = "appstatistics" + ownerAppearance = "appearance" + ownerApplications = "applications" + ownerBroadcastMessages = "broadcastmessages" + ownerBulkImports = "bulkimports" + ownerClusterAgents = "clusteragents" + ownerCustomAttributes = "customattributes" + ownerDBMigrations = "dbmigrations" + ownerDependencyProxy = "dependencyproxy" + ownerErrorTracking = "errortracking" + ownerFeatures = "features" + ownerImportService = "importservice" + ownerLicense = "license" + ownerMetadata = "metadata" + ownerPlanLimits = "planlimits" + ownerSecureFiles = "securefiles" + ownerSettings = "settings" + ownerSidekiq = "sidekiq" + ownerSystemHooks = "systemhooks" + ownerTerraformStates = "terraformstates" + ownerTopics = "topics" + ownerUsageData = "usagedata" +) + const ( actionSystemHookGet = "admin.system_hook_get" actionSettingsGet = "admin.settings_get" @@ -95,30 +134,30 @@ const ( // ActionSpecs returns canonical specs for gitlab_admin meta-tool actions. func ActionSpecs(client *gitlabclient.Client) []toolutil.ActionSpec { return []toolutil.ActionSpec{ - adminReadSpec("topic_list", toolutil.RouteAction(client, topics.List), "gitlab_list_topics"), - adminReadSpec("topic_get", toolutil.RouteAction(client, topics.Get), "gitlab_get_topic"), - adminCreateSpec("topic_create", toolutil.RouteAction(client, topics.Create), "gitlab_create_topic"), - adminUpdateSpec("topic_update", toolutil.RouteAction(client, topics.Update), "gitlab_update_topic"), - adminDeleteSpec("topic_delete", toolutil.DestructiveVoidAction(client, topics.Delete), "gitlab_delete_topic"), + adminReadSpec("topic_list", ownerTopics, toolutil.RouteAction(client, topics.List), "gitlab_list_topics"), + adminReadSpec("topic_get", ownerTopics, toolutil.RouteAction(client, topics.Get), "gitlab_get_topic"), + adminCreateSpec("topic_create", ownerTopics, toolutil.RouteAction(client, topics.Create), "gitlab_create_topic"), + adminUpdateSpec("topic_update", ownerTopics, toolutil.RouteAction(client, topics.Update), "gitlab_update_topic"), + adminDeleteSpec("topic_delete", ownerTopics, toolutil.DestructiveVoidAction(client, topics.Delete), "gitlab_delete_topic"), adminSettingsGetSpec(client), - adminUpdateSpec("settings_update", toolutil.RouteAction(client, settings.Update), "gitlab_update_settings"), + adminUpdateSpec("settings_update", ownerSettings, toolutil.RouteAction(client, settings.Update), "gitlab_update_settings"), adminAppearanceGetSpec(client), adminAppearanceUpdateSpec(client), - adminReadSpec("broadcast_message_list", toolutil.RouteAction(client, broadcastmessages.List), "gitlab_list_broadcast_messages"), - adminReadSpec("broadcast_message_get", toolutil.RouteAction(client, broadcastmessages.Get), "gitlab_get_broadcast_message"), - adminCreateSpec("broadcast_message_create", toolutil.RouteAction(client, broadcastmessages.Create), "gitlab_create_broadcast_message"), - adminUpdateSpec("broadcast_message_update", toolutil.RouteAction(client, broadcastmessages.Update), "gitlab_update_broadcast_message"), - adminDeleteSpec("broadcast_message_delete", toolutil.DestructiveAction(client, broadcastmessages.DeleteOutput), "gitlab_delete_broadcast_message"), - adminReadSpec("feature_list", toolutil.RouteAction(client, features.List), "gitlab_list_features"), - adminReadSpec("feature_list_definitions", toolutil.RouteAction(client, features.ListDefinitions), "gitlab_list_feature_definitions"), - adminUpdateCreateIndividualSpec("feature_set", features.SetRoute(client), "gitlab_set_feature_flag"), - adminDeleteSpec("feature_delete", toolutil.DestructiveVoidAction(client, features.Delete), "gitlab_delete_feature_flag"), - adminReadSpec("license_get", toolutil.RouteAction(client, license.Get), "gitlab_get_license"), - adminCreateSpec("license_add", toolutil.RouteAction(client, license.Add), "gitlab_add_license"), - adminDeleteSpec("license_delete", toolutil.DestructiveVoidAction(client, license.Delete), "gitlab_delete_license"), - adminReadSpec("system_hook_list", toolutil.RouteAction(client, systemhooks.List), "gitlab_list_system_hooks"), - adminReadSpec("system_hook_get", toolutil.RouteAction(client, systemhooks.Get), "gitlab_get_system_hook"), - adminCreateSpec("system_hook_add", toolutil.RouteAction(client, systemhooks.Add), "gitlab_add_system_hook"), + adminReadSpec("broadcast_message_list", ownerBroadcastMessages, toolutil.RouteAction(client, broadcastmessages.List), "gitlab_list_broadcast_messages"), + adminReadSpec("broadcast_message_get", ownerBroadcastMessages, toolutil.RouteAction(client, broadcastmessages.Get), "gitlab_get_broadcast_message"), + adminCreateSpec("broadcast_message_create", ownerBroadcastMessages, toolutil.RouteAction(client, broadcastmessages.Create), "gitlab_create_broadcast_message"), + adminUpdateSpec("broadcast_message_update", ownerBroadcastMessages, toolutil.RouteAction(client, broadcastmessages.Update), "gitlab_update_broadcast_message"), + adminDeleteSpec("broadcast_message_delete", ownerBroadcastMessages, toolutil.DestructiveAction(client, broadcastmessages.DeleteOutput), "gitlab_delete_broadcast_message"), + adminReadSpec("feature_list", ownerFeatures, toolutil.RouteAction(client, features.List), "gitlab_list_features"), + adminReadSpec("feature_list_definitions", ownerFeatures, toolutil.RouteAction(client, features.ListDefinitions), "gitlab_list_feature_definitions"), + adminUpdateCreateIndividualSpec("feature_set", ownerFeatures, features.SetRoute(client), "gitlab_set_feature_flag"), + adminDeleteSpec("feature_delete", ownerFeatures, toolutil.DestructiveVoidAction(client, features.Delete), "gitlab_delete_feature_flag"), + adminReadSpec("license_get", ownerLicense, toolutil.RouteAction(client, license.Get), "gitlab_get_license"), + adminCreateSpec("license_add", ownerLicense, toolutil.RouteAction(client, license.Add), "gitlab_add_license"), + adminDeleteSpec("license_delete", ownerLicense, toolutil.DestructiveVoidAction(client, license.Delete), "gitlab_delete_license"), + adminReadSpec("system_hook_list", ownerSystemHooks, toolutil.RouteAction(client, systemhooks.List), "gitlab_list_system_hooks"), + adminReadSpec("system_hook_get", ownerSystemHooks, toolutil.RouteAction(client, systemhooks.Get), "gitlab_get_system_hook"), + adminCreateSpec("system_hook_add", ownerSystemHooks, toolutil.RouteAction(client, systemhooks.Add), "gitlab_add_system_hook"), adminSystemHookEditSpec(client), // Classified as a create, not a read, and deliberately without an // annotation override. Firing a test event makes GitLab POST a sample @@ -127,126 +166,126 @@ func ActionSpecs(client *gitlabclient.Client) []toolutil.ActionSpec { // The override that used to claim read-only here was inert anyway: // IndividualToolAnnotationOverrides.NarrowingOnly drops any claim that // widens the base classification, so it never reached a served tool. - adminCreateSpec("system_hook_test", toolutil.RouteAction(client, systemhooks.Test), "gitlab_test_system_hook"), + adminCreateSpec("system_hook_test", ownerSystemHooks, toolutil.RouteAction(client, systemhooks.Test), "gitlab_test_system_hook"), adminSystemHookSetURLVariableSpec(client), adminSystemHookDeleteURLVariableSpec(client), - adminDeleteSpec("system_hook_delete", toolutil.DestructiveVoidAction(client, systemhooks.Delete), "gitlab_delete_system_hook"), - adminReadSpec("sidekiq_queue_metrics", toolutil.RouteAction(client, sidekiq.GetQueueMetrics), "gitlab_get_sidekiq_queue_metrics"), - adminReadSpec("sidekiq_process_metrics", toolutil.RouteAction(client, sidekiq.GetProcessMetrics), "gitlab_get_sidekiq_process_metrics"), - adminReadSpec("sidekiq_job_stats", toolutil.RouteAction(client, sidekiq.GetJobStats), "gitlab_get_sidekiq_job_stats"), - adminReadSpec("sidekiq_compound_metrics", toolutil.RouteAction(client, sidekiq.GetCompoundMetrics), "gitlab_get_sidekiq_compound_metrics"), - adminReadSpec("plan_limits_get", toolutil.RouteAction(client, planlimits.Get), "gitlab_get_plan_limits"), - adminUpdateSpec("plan_limits_change", toolutil.RouteAction(client, planlimits.Change), "gitlab_change_plan_limits"), - adminReadSpec("usage_data_service_ping", toolutil.RouteAction(client, usagedata.GetServicePing), "gitlab_get_service_ping"), - adminReadSpec("usage_data_non_sql_metrics", toolutil.RouteAction(client, usagedata.GetNonSQLMetrics), "gitlab_get_non_sql_metrics"), - adminReadSpec("usage_data_queries", toolutil.RouteAction(client, usagedata.GetQueries), "gitlab_get_usage_queries"), - adminReadSpec("usage_data_metric_definitions", toolutil.RouteAction(client, usagedata.GetMetricDefinitions), "gitlab_get_metric_definitions"), - adminCreateSpec("usage_data_track_event", toolutil.RouteAction(client, usagedata.TrackEvent), "gitlab_track_event"), - adminCreateSpec("usage_data_track_events", toolutil.RouteAction(client, usagedata.TrackEvents), "gitlab_track_events"), - adminDestructiveUpdateIndividualSpec("db_migration_mark", toolutil.DestructiveAction(client, dbmigrations.Mark), "gitlab_mark_migration"), - adminReadSpec("application_list", toolutil.RouteAction(client, applications.List), "gitlab_list_applications"), - adminCreateSpec("application_create", toolutil.RouteAction(client, applications.Create), "gitlab_create_application"), - adminCreateSpec("application_renew_secret", toolutil.RouteAction(client, applications.RenewSecret), "gitlab_renew_application_secret"), - adminDeleteSpec("application_delete", toolutil.DestructiveVoidAction(client, applications.Delete), "gitlab_delete_application"), + adminDeleteSpec("system_hook_delete", ownerSystemHooks, toolutil.DestructiveVoidAction(client, systemhooks.Delete), "gitlab_delete_system_hook"), + adminReadSpec("sidekiq_queue_metrics", ownerSidekiq, toolutil.RouteAction(client, sidekiq.GetQueueMetrics), "gitlab_get_sidekiq_queue_metrics"), + adminReadSpec("sidekiq_process_metrics", ownerSidekiq, toolutil.RouteAction(client, sidekiq.GetProcessMetrics), "gitlab_get_sidekiq_process_metrics"), + adminReadSpec("sidekiq_job_stats", ownerSidekiq, toolutil.RouteAction(client, sidekiq.GetJobStats), "gitlab_get_sidekiq_job_stats"), + adminReadSpec("sidekiq_compound_metrics", ownerSidekiq, toolutil.RouteAction(client, sidekiq.GetCompoundMetrics), "gitlab_get_sidekiq_compound_metrics"), + adminReadSpec("plan_limits_get", ownerPlanLimits, toolutil.RouteAction(client, planlimits.Get), "gitlab_get_plan_limits"), + adminUpdateSpec("plan_limits_change", ownerPlanLimits, toolutil.RouteAction(client, planlimits.Change), "gitlab_change_plan_limits"), + adminReadSpec("usage_data_service_ping", ownerUsageData, toolutil.RouteAction(client, usagedata.GetServicePing), "gitlab_get_service_ping"), + adminReadSpec("usage_data_non_sql_metrics", ownerUsageData, toolutil.RouteAction(client, usagedata.GetNonSQLMetrics), "gitlab_get_non_sql_metrics"), + adminReadSpec("usage_data_queries", ownerUsageData, toolutil.RouteAction(client, usagedata.GetQueries), "gitlab_get_usage_queries"), + adminReadSpec("usage_data_metric_definitions", ownerUsageData, toolutil.RouteAction(client, usagedata.GetMetricDefinitions), "gitlab_get_metric_definitions"), + adminCreateSpec("usage_data_track_event", ownerUsageData, toolutil.RouteAction(client, usagedata.TrackEvent), "gitlab_track_event"), + adminCreateSpec("usage_data_track_events", ownerUsageData, toolutil.RouteAction(client, usagedata.TrackEvents), "gitlab_track_events"), + adminDestructiveUpdateIndividualSpec("db_migration_mark", ownerDBMigrations, toolutil.DestructiveAction(client, dbmigrations.Mark), "gitlab_mark_migration"), + adminReadSpec("application_list", ownerApplications, toolutil.RouteAction(client, applications.List), "gitlab_list_applications"), + adminCreateSpec("application_create", ownerApplications, toolutil.RouteAction(client, applications.Create), "gitlab_create_application"), + adminCreateSpec("application_renew_secret", ownerApplications, toolutil.RouteAction(client, applications.RenewSecret), "gitlab_renew_application_secret"), + adminDeleteSpec("application_delete", ownerApplications, toolutil.DestructiveVoidAction(client, applications.Delete), "gitlab_delete_application"), adminApplicationStatisticsGetSpec(client), adminMetadataGetSpec(client), - adminReadSpec("custom_attr_list", toolutil.RouteAction(client, customattributes.List), "gitlab_list_custom_attributes"), - adminReadSpec("custom_attr_get", toolutil.RouteAction(client, customattributes.Get), "gitlab_get_custom_attribute"), - adminUpdateCreateIndividualSpec("custom_attr_set", toolutil.RouteAction(client, customattributes.Set), "gitlab_set_custom_attribute"), - adminDeleteSpec("custom_attr_delete", toolutil.DestructiveAction(client, customattributes.DeleteOutput), "gitlab_delete_custom_attribute"), - adminCreateSpec("bulk_import_start", toolutil.RouteAction(client, bulkimports.StartMigration), "gitlab_start_bulk_import"), - adminReadSpec("bulk_import_list", toolutil.RouteAction(client, bulkimports.List), "gitlab_list_bulk_imports"), - adminReadSpec("bulk_import_get", toolutil.RouteAction(client, bulkimports.Get), "gitlab_get_bulk_import"), - adminUpdateSpec("bulk_import_cancel", toolutil.RouteAction(client, bulkimports.Cancel), "gitlab_cancel_bulk_import"), - adminReadSpec("bulk_import_entity_list", toolutil.RouteAction(client, bulkimports.ListEntities), "gitlab_list_bulk_import_entities"), - adminReadSpec("bulk_import_entity_get", toolutil.RouteAction(client, bulkimports.GetEntity), "gitlab_get_bulk_import_entity"), - adminReadSpec("bulk_import_entity_failures", toolutil.RouteAction(client, bulkimports.ListEntityFailures), "gitlab_list_bulk_import_entity_failures"), - adminReadSpec("error_tracking_list", toolutil.RouteAction(client, errortracking.ListClientKeys), "gitlab_list_error_tracking_client_keys"), - adminCreateSpec("error_tracking_create", toolutil.RouteAction(client, errortracking.CreateClientKey), "gitlab_create_error_tracking_client_key"), - adminDeleteSpec("error_tracking_delete", toolutil.DestructiveVoidAction(client, errortracking.DeleteClientKey), "gitlab_delete_error_tracking_client_key"), - adminReadSpec("error_tracking_get_settings", toolutil.RouteAction(client, errortracking.GetSettings), "gitlab_get_error_tracking_settings"), - adminUpdateSpec("error_tracking_update_settings", toolutil.RouteAction(client, errortracking.EnableDisable), "gitlab_enable_disable_error_tracking"), - adminReadSpec("alert_metric_image_list", toolutil.RouteAction(client, alertmanagement.ListMetricImages), "gitlab_list_alert_metric_images"), - adminCreateSpec("alert_metric_image_upload", toolutil.RouteAction(client, alertmanagement.UploadMetricImage), "gitlab_upload_alert_metric_image"), - adminUpdateSpec("alert_metric_image_update", toolutil.RouteAction(client, alertmanagement.UpdateMetricImage), "gitlab_update_alert_metric_image"), - adminDeleteSpec("alert_metric_image_delete", toolutil.DestructiveVoidAction(client, alertmanagement.DeleteMetricImage), "gitlab_delete_alert_metric_image"), - adminReadSpec("secure_file_list", toolutil.RouteAction(client, securefiles.List), "gitlab_list_secure_files"), - adminReadSpec("secure_file_get", toolutil.RouteAction(client, securefiles.Show), "gitlab_show_secure_file"), - adminCreateSpec("secure_file_create", toolutil.RouteAction(client, securefiles.Create), "gitlab_create_secure_file"), - adminDeleteSpec("secure_file_delete", toolutil.DestructiveVoidAction(client, securefiles.Remove), "gitlab_remove_secure_file"), - adminReadSpec("terraform_state_list", toolutil.RouteAction(client, terraformstates.List), "gitlab_list_terraform_states"), - adminReadSpec("terraform_state_get", toolutil.RouteAction(client, terraformstates.Get), "gitlab_get_terraform_state"), - adminDeleteSpec("terraform_state_delete", toolutil.DestructiveVoidAction(client, terraformstates.Delete), "gitlab_delete_terraform_state"), - adminUpdateSpec("terraform_state_lock", toolutil.RouteAction(client, terraformstates.Lock), "gitlab_lock_terraform_state"), + adminReadSpec("custom_attr_list", ownerCustomAttributes, toolutil.RouteAction(client, customattributes.List), "gitlab_list_custom_attributes"), + adminReadSpec("custom_attr_get", ownerCustomAttributes, toolutil.RouteAction(client, customattributes.Get), "gitlab_get_custom_attribute"), + adminUpdateCreateIndividualSpec("custom_attr_set", ownerCustomAttributes, toolutil.RouteAction(client, customattributes.Set), "gitlab_set_custom_attribute"), + adminDeleteSpec("custom_attr_delete", ownerCustomAttributes, toolutil.DestructiveAction(client, customattributes.DeleteOutput), "gitlab_delete_custom_attribute"), + adminCreateSpec("bulk_import_start", ownerBulkImports, toolutil.RouteAction(client, bulkimports.StartMigration), "gitlab_start_bulk_import"), + adminReadSpec("bulk_import_list", ownerBulkImports, toolutil.RouteAction(client, bulkimports.List), "gitlab_list_bulk_imports"), + adminReadSpec("bulk_import_get", ownerBulkImports, toolutil.RouteAction(client, bulkimports.Get), "gitlab_get_bulk_import"), + adminUpdateSpec("bulk_import_cancel", ownerBulkImports, toolutil.RouteAction(client, bulkimports.Cancel), "gitlab_cancel_bulk_import"), + adminReadSpec("bulk_import_entity_list", ownerBulkImports, toolutil.RouteAction(client, bulkimports.ListEntities), "gitlab_list_bulk_import_entities"), + adminReadSpec("bulk_import_entity_get", ownerBulkImports, toolutil.RouteAction(client, bulkimports.GetEntity), "gitlab_get_bulk_import_entity"), + adminReadSpec("bulk_import_entity_failures", ownerBulkImports, toolutil.RouteAction(client, bulkimports.ListEntityFailures), "gitlab_list_bulk_import_entity_failures"), + adminReadSpec("error_tracking_list", ownerErrorTracking, toolutil.RouteAction(client, errortracking.ListClientKeys), "gitlab_list_error_tracking_client_keys"), + adminCreateSpec("error_tracking_create", ownerErrorTracking, toolutil.RouteAction(client, errortracking.CreateClientKey), "gitlab_create_error_tracking_client_key"), + adminDeleteSpec("error_tracking_delete", ownerErrorTracking, toolutil.DestructiveVoidAction(client, errortracking.DeleteClientKey), "gitlab_delete_error_tracking_client_key"), + adminReadSpec("error_tracking_get_settings", ownerErrorTracking, toolutil.RouteAction(client, errortracking.GetSettings), "gitlab_get_error_tracking_settings"), + adminUpdateSpec("error_tracking_update_settings", ownerErrorTracking, toolutil.RouteAction(client, errortracking.EnableDisable), "gitlab_enable_disable_error_tracking"), + adminReadSpec("alert_metric_image_list", ownerAlertManagement, toolutil.RouteAction(client, alertmanagement.ListMetricImages), "gitlab_list_alert_metric_images"), + adminCreateSpec("alert_metric_image_upload", ownerAlertManagement, toolutil.RouteAction(client, alertmanagement.UploadMetricImage), "gitlab_upload_alert_metric_image"), + adminUpdateSpec("alert_metric_image_update", ownerAlertManagement, toolutil.RouteAction(client, alertmanagement.UpdateMetricImage), "gitlab_update_alert_metric_image"), + adminDeleteSpec("alert_metric_image_delete", ownerAlertManagement, toolutil.DestructiveVoidAction(client, alertmanagement.DeleteMetricImage), "gitlab_delete_alert_metric_image"), + adminReadSpec("secure_file_list", ownerSecureFiles, toolutil.RouteAction(client, securefiles.List), "gitlab_list_secure_files"), + adminReadSpec("secure_file_get", ownerSecureFiles, toolutil.RouteAction(client, securefiles.Show), "gitlab_show_secure_file"), + adminCreateSpec("secure_file_create", ownerSecureFiles, toolutil.RouteAction(client, securefiles.Create), "gitlab_create_secure_file"), + adminDeleteSpec("secure_file_delete", ownerSecureFiles, toolutil.DestructiveVoidAction(client, securefiles.Remove), "gitlab_remove_secure_file"), + adminReadSpec("terraform_state_list", ownerTerraformStates, toolutil.RouteAction(client, terraformstates.List), "gitlab_list_terraform_states"), + adminReadSpec("terraform_state_get", ownerTerraformStates, toolutil.RouteAction(client, terraformstates.Get), "gitlab_get_terraform_state"), + adminDeleteSpec("terraform_state_delete", ownerTerraformStates, toolutil.DestructiveVoidAction(client, terraformstates.Delete), "gitlab_delete_terraform_state"), + adminUpdateSpec("terraform_state_lock", ownerTerraformStates, toolutil.RouteAction(client, terraformstates.Lock), "gitlab_lock_terraform_state"), adminTerraformStateUnlockSpec(client), - adminDeleteSpec("terraform_version_delete", toolutil.DestructiveVoidAction(client, terraformstates.DeleteVersion), "gitlab_delete_terraform_state_version"), - adminReadSpec("cluster_agent_list", toolutil.RouteAction(client, clusteragents.ListAgents), "gitlab_list_cluster_agents"), - adminReadSpec("cluster_agent_get", toolutil.RouteAction(client, clusteragents.GetAgent), "gitlab_get_cluster_agent"), - adminCreateSpec("cluster_agent_register", toolutil.RouteAction(client, clusteragents.RegisterAgent), "gitlab_register_cluster_agent"), - adminDeleteSpec("cluster_agent_delete", toolutil.DestructiveVoidAction(client, clusteragents.DeleteAgent), "gitlab_delete_cluster_agent"), - adminReadSpec("cluster_agent_token_list", toolutil.RouteAction(client, clusteragents.ListAgentTokens), "gitlab_list_cluster_agent_tokens"), - adminReadSpec("cluster_agent_token_get", toolutil.RouteAction(client, clusteragents.GetAgentToken), "gitlab_get_cluster_agent_token"), - adminCreateSpec("cluster_agent_token_create", toolutil.RouteAction(client, clusteragents.CreateAgentToken), "gitlab_create_cluster_agent_token"), - adminDeleteSpec("cluster_agent_token_revoke", toolutil.DestructiveVoidAction(client, clusteragents.RevokeAgentToken), "gitlab_revoke_cluster_agent_token"), - adminDeleteSpec("dependency_proxy_delete", toolutil.DestructiveVoidAction(client, dependencyproxy.Purge), "gitlab_purge_dependency_proxy"), - adminCreateSpec("import_github", toolutil.RouteAction(client, importservice.ImportFromGitHub), "gitlab_import_from_github"), - adminUpdateSpec("import_cancel_github", toolutil.RouteAction(client, importservice.CancelGitHubImport), "gitlab_cancel_github_import"), - adminCreateSpec("import_gists", toolutil.RouteVoidAction(client, importservice.ImportGists), "gitlab_import_github_gists"), - adminCreateSpec("import_bitbucket", toolutil.RouteAction(client, importservice.ImportFromBitbucketCloud), "gitlab_import_from_bitbucket_cloud"), - adminCreateSpec("import_bitbucket_server", toolutil.RouteAction(client, importservice.ImportFromBitbucketServer), "gitlab_import_from_bitbucket_server"), + adminDeleteSpec("terraform_version_delete", ownerTerraformStates, toolutil.DestructiveVoidAction(client, terraformstates.DeleteVersion), "gitlab_delete_terraform_state_version"), + adminReadSpec("cluster_agent_list", ownerClusterAgents, toolutil.RouteAction(client, clusteragents.ListAgents), "gitlab_list_cluster_agents"), + adminReadSpec("cluster_agent_get", ownerClusterAgents, toolutil.RouteAction(client, clusteragents.GetAgent), "gitlab_get_cluster_agent"), + adminCreateSpec("cluster_agent_register", ownerClusterAgents, toolutil.RouteAction(client, clusteragents.RegisterAgent), "gitlab_register_cluster_agent"), + adminDeleteSpec("cluster_agent_delete", ownerClusterAgents, toolutil.DestructiveVoidAction(client, clusteragents.DeleteAgent), "gitlab_delete_cluster_agent"), + adminReadSpec("cluster_agent_token_list", ownerClusterAgents, toolutil.RouteAction(client, clusteragents.ListAgentTokens), "gitlab_list_cluster_agent_tokens"), + adminReadSpec("cluster_agent_token_get", ownerClusterAgents, toolutil.RouteAction(client, clusteragents.GetAgentToken), "gitlab_get_cluster_agent_token"), + adminCreateSpec("cluster_agent_token_create", ownerClusterAgents, toolutil.RouteAction(client, clusteragents.CreateAgentToken), "gitlab_create_cluster_agent_token"), + adminDeleteSpec("cluster_agent_token_revoke", ownerClusterAgents, toolutil.DestructiveVoidAction(client, clusteragents.RevokeAgentToken), "gitlab_revoke_cluster_agent_token"), + adminDeleteSpec("dependency_proxy_delete", ownerDependencyProxy, toolutil.DestructiveVoidAction(client, dependencyproxy.Purge), "gitlab_purge_dependency_proxy"), + adminCreateSpec("import_github", ownerImportService, toolutil.RouteAction(client, importservice.ImportFromGitHub), "gitlab_import_from_github"), + adminUpdateSpec("import_cancel_github", ownerImportService, toolutil.RouteAction(client, importservice.CancelGitHubImport), "gitlab_cancel_github_import"), + adminCreateSpec("import_gists", ownerImportService, toolutil.RouteVoidAction(client, importservice.ImportGists), "gitlab_import_github_gists"), + adminCreateSpec("import_bitbucket", ownerImportService, toolutil.RouteAction(client, importservice.ImportFromBitbucketCloud), "gitlab_import_from_bitbucket_cloud"), + adminCreateSpec("import_bitbucket_server", ownerImportService, toolutil.RouteAction(client, importservice.ImportFromBitbucketServer), "gitlab_import_from_bitbucket_server"), } } -func adminReadSpec(name string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { - return toolutil.NewReadActionSpec(name, route, adminOptions(individualTool)) +func adminReadSpec(name, owner string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { + return toolutil.NewReadActionSpec(name, route, adminOptions(owner, individualTool)) } -func adminCreateSpec(name string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { - return toolutil.NewCreateActionSpec(name, route, adminOptions(individualTool)) +func adminCreateSpec(name, owner string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { + return toolutil.NewCreateActionSpec(name, route, adminOptions(owner, individualTool)) } -func adminUpdateSpec(name string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { - return toolutil.NewUpdateActionSpec(name, route, adminOptions(individualTool)) +func adminUpdateSpec(name, owner string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { + return toolutil.NewUpdateActionSpec(name, route, adminOptions(owner, individualTool)) } -func adminDeleteSpec(name string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { - return toolutil.NewDeleteActionSpec(name, route, adminOptions(individualTool)) +func adminDeleteSpec(name, owner string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { + return toolutil.NewDeleteActionSpec(name, route, adminOptions(owner, individualTool)) } -func adminUpdateCreateIndividualSpec(name string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { +func adminUpdateCreateIndividualSpec(name, owner string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { individualIdempotent := false - options := adminOptions(individualTool) + options := adminOptions(owner, individualTool) options.IndividualTool.AnnotationOverrides.Idempotent = &individualIdempotent return toolutil.NewUpdateActionSpec(name, route, options) } -func adminDestructiveUpdateIndividualSpec(name string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { +func adminDestructiveUpdateIndividualSpec(name, owner string, route toolutil.ActionRoute, individualTool string) toolutil.ActionSpec { individualDestructive := false - options := adminOptions(individualTool) + options := adminOptions(owner, individualTool) options.IndividualTool.AnnotationOverrides.Destructive = &individualDestructive return toolutil.NewDeleteActionSpec(name, route, options) } func adminSystemHookEditSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_edit_system_hook") + options := adminOptions(ownerSystemHooks, "gitlab_edit_system_hook") options.IndividualTool.Description = "Edit an instance system hook, including event triggers, SSL verification, and URL settings. Returns: the updated system hook object. See also: gitlab_get_system_hook, gitlab_list_system_hooks, gitlab_test_system_hook." return toolutil.NewUpdateActionSpec("system_hook_edit", toolutil.RouteAction(client, systemhooks.Edit), options) } func adminSystemHookSetURLVariableSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_set_system_hook_url_variable") + options := adminOptions(ownerSystemHooks, "gitlab_set_system_hook_url_variable") options.IndividualTool.Description = "Create or update one URL variable for an instance system hook. Returns: a success status and message naming the variable key. See also: gitlab_edit_system_hook, gitlab_get_system_hook." return toolutil.NewUpdateActionSpec("system_hook_set_url_variable", toolutil.RouteVoidAction(client, systemhooks.SetURLVariable), options) } func adminSystemHookDeleteURLVariableSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_delete_system_hook_url_variable") + options := adminOptions(ownerSystemHooks, "gitlab_delete_system_hook_url_variable") options.IndividualTool.Description = "Delete one URL variable from an instance system hook. Returns: a success status and message naming the variable key. See also: gitlab_set_system_hook_url_variable, gitlab_get_system_hook." return toolutil.NewDeleteActionSpec("system_hook_delete_url_variable", toolutil.DestructiveVoidAction(client, systemhooks.DeleteURLVariable), options) } func adminSettingsGetSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_get_settings") + options := adminOptions(ownerSettings, "gitlab_get_settings") options.Usage = "Read current GitLab application settings. Use this for instance or application settings, not for server metadata or version information." options.Aliases = []string{"application settings", "instance settings", "current settings", "admin settings", "gitlab settings"} options.Tags = append(options.Tags, "settings", "application_settings") @@ -256,7 +295,7 @@ func adminSettingsGetSpec(client *gitlabclient.Client) toolutil.ActionSpec { } func adminMetadataGetSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_get_metadata") + options := adminOptions(ownerMetadata, "gitlab_get_metadata") options.Usage = "Read GitLab instance metadata such as version and revision. Do not use this for application settings." options.Aliases = []string{"instance metadata", "gitlab version", "server metadata", "gitlab revision"} options.Tags = append(options.Tags, "metadata", "version") @@ -266,7 +305,7 @@ func adminMetadataGetSpec(client *gitlabclient.Client) toolutil.ActionSpec { } func adminAppearanceGetSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_get_appearance") + options := adminOptions(ownerAppearance, "gitlab_get_appearance") options.Usage = "Read the current GitLab application appearance and branding settings. Use this for logos, banners, PWA labels, and instance message colors rather than general application settings or version metadata." options.Aliases = []string{tagAppearance, "application appearance", "instance appearance", "branding settings", "gitlab appearance"} options.Tags = append(options.Tags, tagAppearance, "branding") @@ -276,7 +315,7 @@ func adminAppearanceGetSpec(client *gitlabclient.Client) toolutil.ActionSpec { } func adminAppearanceUpdateSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_update_appearance") + options := adminOptions(ownerAppearance, "gitlab_update_appearance") options.Usage = "Update GitLab application appearance and branding settings such as title, messages, colors, PWA labels, and profile guidance text. Requires administrator access and changes the instance UI immediately." options.Aliases = []string{"update appearance", "change appearance", "update branding", "change branding", "appearance settings update"} options.Tags = append(options.Tags, tagAppearance, "branding") @@ -298,7 +337,7 @@ func adminAppearanceUpdateSpec(client *gitlabclient.Client) toolutil.ActionSpec } func adminApplicationStatisticsGetSpec(client *gitlabclient.Client) toolutil.ActionSpec { - options := adminOptions("gitlab_get_application_statistics") + options := adminOptions(ownerAppStatistics, "gitlab_get_application_statistics") options.Usage = "Read GitLab instance-wide application statistics such as totals for users, groups, projects, issues, and merge requests. Requires administrator access." options.Aliases = []string{"application statistics", "instance statistics", "gitlab statistics", "admin statistics"} options.Tags = append(options.Tags, "statistics", "instance") @@ -309,7 +348,7 @@ func adminApplicationStatisticsGetSpec(client *gitlabclient.Client) toolutil.Act func adminTerraformStateUnlockSpec(client *gitlabclient.Client) toolutil.ActionSpec { individualDestructive := false - options := adminOptions("gitlab_unlock_terraform_state") + options := adminOptions(ownerTerraformStates, "gitlab_unlock_terraform_state") options.IndividualTool.AnnotationOverrides.Destructive = &individualDestructive options.Tags = append(options.Tags, "terraform", "terraform_state", "state", "lock", "unlock") options.Usage = "Unlock a GitLab Terraform state by project_id and state name. Use params.name for the Terraform state name. Do not send the state name as id." @@ -331,11 +370,14 @@ func adminTerraformStateUnlockSpec(client *gitlabclient.Client) toolutil.ActionS return toolutil.NewDeleteActionSpec("terraform_state_unlock", toolutil.DestructiveAction(client, terraformstates.Unlock), options) } -func adminOptions(individualTool string) toolutil.ActionSpecOptions { +// adminOptions builds the shared options for one admin action. The owner is +// the domain package whose handler the action routes to, never this one: see +// the owner constants above for why the distinction is load-bearing. +func adminOptions(owner, individualTool string) toolutil.ActionSpecOptions { options := toolutil.ActionSpecOptions{ Aliases: []string{individualTool}, Usage: "Use to execute adminspecs domain action.", Tags: []string{"admin"}, OpenWorld: true, - OwnerPackage: "adminspecs", + OwnerPackage: owner, IndividualTool: toolutil.IndividualToolSpec{Name: individualTool, Title: toolutil.TitleFromName(individualTool)}, } decorateAdminMeta(&options, individualTool) diff --git a/internal/tools/adminspecs/action_specs_test.go b/internal/tools/adminspecs/action_specs_test.go index 5a400c8a8..452c69dbb 100644 --- a/internal/tools/adminspecs/action_specs_test.go +++ b/internal/tools/adminspecs/action_specs_test.go @@ -1,6 +1,7 @@ package adminspecs import ( + "path" "slices" "strings" "testing" @@ -24,8 +25,8 @@ func TestActionSpecs_MetadataInvariants(t *testing.T) { } names[spec.Name] = true - if spec.OwnerPackage != "adminspecs" { - t.Fatalf("%s OwnerPackage = %q, want adminspecs", spec.Name, spec.OwnerPackage) + if spec.OwnerPackage == "" { + t.Fatalf("%s OwnerPackage is empty", spec.Name) } if !spec.OpenWorld { t.Fatalf("%s OpenWorld = false, want true", spec.Name) @@ -55,6 +56,37 @@ func TestActionSpecs_MetadataInvariants(t *testing.T) { } } +// TestActionSpecs_OwnerPackage_IsThePackageTheHandlerComesFrom verifies that +// every admin action names the domain package whose handler it routes to, +// rather than this one, which declares the specs and issues no request of its +// own. +// +// The owner is a join key rather than a label: the request inventory records a +// package and nothing else, so an action owned by a package that issues nothing +// can be joined to no recording at all, and one owned by the wrong package is +// quietly credited with somebody else's requests. That second error is the one +// R-PATH cannot catch by itself, which is why it is caught here. +// +// The oracle is the route rather than a second table, so this asserts a fact +// about the tree instead of restating the constants it checks: every admin +// handler takes the input type its own package declares, so the package path of +// Route.InputType names the package that will issue the request. A +// copy-pasted owner constant therefore fails here. +func TestActionSpecs_OwnerPackage_IsThePackageTheHandlerComesFrom(t *testing.T) { + for _, spec := range ActionSpecs(nil) { + t.Run(spec.Name, func(t *testing.T) { + if spec.Route.InputType == nil { + t.Fatalf("%s carries no input type to read an owner from", spec.Name) + } + pkgPath := spec.Route.InputType.PkgPath() + want := path.Base(pkgPath) + if spec.OwnerPackage != want { + t.Errorf("OwnerPackage = %q, want %q: the handler's input type is %s", spec.OwnerPackage, want, pkgPath) + } + }) + } +} + // TestActionSpecs_SelectedActionSemantics verifies representative admin actions // retain their canonical read-only, destructive, and idempotency classifications. func TestActionSpecs_SelectedActionSemantics(t *testing.T) {