backstage / backstage/community-plugins
๐ copilot-backend: enterprise metrics always 403 with GitHub App auth โ installation resolved via getOrgInstallation
- Dominant language
- TypeScript
- Stars
- 422
- Forks
- 697
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 286
Description
### Workspace
copilot
### ๐ Description
When the copilot plugin is configured with GitHub App credentials and `copilot.enterprise`, enterprise metrics ingestion fails on every scheduled run with:
```
[TaskManagementV2] Failed ingest for enterprise: on . Resource not accessible by integration - https://docs.github.com/rest/copilot/copilot-usage-metrics#get-copilot-enterprise-usage-metrics-for-a-specific-day
```
The root cause is in `GithubClientV2.getOctokit` (workspaces/copilot/plugins/copilot-backend/src/client/GithubClientV2.ts):
```ts
const orgName =
type === 'organization'
? this.copilotConfig.organization
: this.copilotConfig.enterprise;
...
const { data: installation } =
await appOctokit.rest.apps.getOrgInstallation({ org: orgName });
```
For the `enterprise` scope the code passes the **enterprise slug** into `GET /orgs/{slug}/installation`. That endpoint can only ever return an **organization** installation. GitHub's enterprise permissions (including "Enterprise Copilot metrics: Read") only attach to installations with `target_type: "Enterprise"` โ organization installations cannot carry them. So the token minted from the org installation is always rejected with `403 Resource not accessible by integration` by the `/enterprises/{slug}/copilot/metrics/reports/*` endpoints, regardless of how the app registration or the enterprise installation is configured.
Two secondary consequences:
- The lookup only "works" at all for customers whose enterprise slug happens to equal one of their org slugs; otherwise `getOrgInstallation` 404s.
- An existing enterprise installation of the same app (with the correct enterprise permissions) is never consulted โ there is no code path that resolves it.
### ๐ Expected behavior
With the GitHub App installed on the enterprise account and granted "Enterprise Copilot metrics: Read-only", enterprise metrics ingestion should succeed using a token minted from the **enterprise** installation.
### ๐ Actual Behavior with Screenshots
Every enterprise-scope ingest fails with `403 Resource not accessible by integration`, even when:
- the app registration requests "Enterprise Copilot metrics: Read-only",
- the app is owned by the enterprise,
- the app is installed on the enterprise account with the permission granted,
- the enterprise "Copilot usage metrics" policy is "Enabled everywhere".
Distributed traces confirm the plugin only ever mints tokens for the org installation ID, never the enterprise installation.
### ๐ Reproduction steps
1. Configure `copilot.enterprise: ` and `integrations.github[].apps` with an app installed on both an org and the enterprise account (enterprise installation granted "Enterprise Copilot metrics: Read-only").
2. Let the scheduled task run.
3. Observe `Failed ingest for enterprise: ... Resource not accessible by integration` for every day, while organization-scope ingestion succeeds.
Note: in our environment the enterprise slug and organization slug are identical. This is why the failure manifests as a 403 (the lookup silently resolves the org installation of the same name) rather than a 404 from getOrgInstallation. Environments where the slugs differ will see a 404 at the installation lookup instead.
### ๐ Provide the context for the Bug.
For the `enterprise` scope, resolve the installation via `GET /app/installations` and select the one targeting the enterprise:
```ts
const installations = await appOctokit.paginate(
appOctokit.rest.apps.listInstallations,
{ per_page: 100 },
);
const target = orgName.toLowerCase();
const enterpriseInstallation = installations.find(
i =>
i.target_type === 'Enterprise' &&
(i.account?.slug?.toLowerCase() === target ||
i.account?.login?.toLowerCase() === target),
);
```
and fail with a descriptive error if no enterprise installation exists. The organization scope is unaffected.
We are running this as a local `yarn patch` against `@backstage-community/plugin-copilot-backend@1.1.0` and enterprise ingest works with it. Happy to open a PR with the fix (plus tests) if maintainers agree with the approach.
Related history: #3240 documented that enterprise previously required a classic PAT (App auth unsupported for enterprise); the current App-auth path added since then contains this defect. #9458 covers separate enterprise v2 parsing bugs downstream of this call โ that issue's fixes don't help if the API call itself 403s.
### ๐ Have you spent some time to check if this bug has been raised before?
- [x] I checked and didn't find similar issue
### ๐ข Have you read the Code of Conduct?
- [x] I have read the [Code of Conduct](https://github.com/backstage/community-plugins/blob/main/CODE_OF_CONDUCT.md)
### Are you willing to submit PR?
Yes I am willing to submit a PR!
Contributor guide
Research direction
Begin in workspaces/copilot/plugins/copilot-backend/src/client/GithubClientV2.ts at getOctokit and trace the enterprise path used by scheduled ingestion. Add tests for selecting a target_type Enterprise installation and for the missing-installation error. Done means enterprise metrics use the enterprise installation while organization behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github, typescript
- Domain
- api, authentication, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100