cli icon indicating copy to clipboard operation
cli copied to clipboard

Detect missing fine grain permission scopes, notify user on steps to take to refresh scopes

Open jjacobgreen opened this issue 1 year ago β€’ 8 comments

Describe the bug

The Github REST API docs specify that creating a PR using a fine-grained permissions token only requires the

"Pull requests" repository permissions (write)

Adding this permission to the fine-grained token also automatically adds the "Metadata" permission as mandatory.

However, this results in an error along the lines of: GraphQL: Resource not accessible by personal access token (repository.defaultBranchRef).

Steps to reproduce the behavior

  1. Create a fine-grained personal access token with the permissions:
    • Pull Requests: Read and write
    • Metadata (mandatory): Read-only
  2. Locally, create a new branch in a repository, make a commit to it and push the branch to the remote
  3. Run gh pr create, either interactively, or with flags
  4. See an error like: GraphQL: Resource not accessible by personal access token (repository.defaultBranchRef)

If the "Contents: Read-only" permission is also added to the fine-grained personal access token, this issue goes away.

Expected vs actual behavior

Expected behaviour: Able to create a PR using the GH CLI using a fine grained permissions token with permissions that are defined in the docs. Actual behaviour: Extra permissions are required.

This seems to be either:

  • An error in the docs
  • REST API docs don't line up with the equivalent action(s) in the GH CLI
  • A bug in the scope of the "Pull Requests: Read and write" permission.

Logs

>> DEBUG=api gh pr create
[git remote -v]
[git config --get-regexp ^remote\..*\.gh-resolved$]
* Request at 2024-08-01 11:47:35.435635 +0100 BST m=+0.183569918
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token [READACTED]
> Content-Length: 405
> Content-Type: application/json; charset=utf-8
> Graphql-Features: merge_queue
> Time-Zone: Europe/London
> User-Agent: GitHub CLI 2.53.0

GraphQL query:
fragment repo on Repository {
    id
    name
    owner { login }
    viewerPermission
    defaultBranchRef {
      name
    }
    isPrivate
  }
  query RepositoryNetwork {
    viewer { login }
    
    [READACTED]: repository(owner: "[READACTED]", name: "[READACTED]") {
      ...repo
      parent {
        ...repo
      }
    }
    
  }
GraphQL variables: null

< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Thu, 01 Aug 2024 10:47:35 GMT
< Github-Authentication-Token-Expiration: 2024-08-31 11:36:27 +0100
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: EA39:9537D:291A3FF:2BBE2CD:66AB67C7
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4980
< X-Ratelimit-Reset: 1722510409
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 20
< X-Xss-Protection: 0

{
  "data": {
    "viewer": {
      "login": "[READACTED]"
    },
    "[READACTED]": {
      "id": "[READACTED]=",
      "name": "[READACTED]",
      "owner": {
        "login": "[READACTED]"
      },
      "viewerPermission": "ADMIN",
      "defaultBranchRef": null,
      "isPrivate": true,
      "parent": null
    }
  },
  "errors": [
    {
      "type": "FORBIDDEN",
      "path": [
        "[READACTED]",
        "defaultBranchRef"
      ],
      "extensions": {
        "saml_failure": false
      },
      "locations": [
        {
          "line": 7,
          "column": 3
        }
      ],
      "message": "Resource not accessible by personal access token"
    }
  ]
}

* Request took 331.800666ms
GraphQL: Resource not accessible by personal access token ([READACTED].defaultBranchRef)

jjacobgreen avatar Aug 01 '24 11:08 jjacobgreen

@jjacobgreen : appreciate you creating this issue and apologies as I imagine this has caused some amount of frustration! πŸ™‡

This seems to be either:

  • An error in the docs
  • REST API docs don't line up with the equivalent action(s) in the GH CLI
  • A bug in the scope of the "Pull Requests: Read and write" permission.

I think an assumption is being made where the GitHub CLI gh pr create command is a 1:1 map to GitHub APIs, which is typically not the case as this command is an attempt to simplify multiple API interactions to create pull requests versus going to the web browser.

Aside from the fact that gh repo clone won't clone the repository with these limited permissions, let's dig into what is going on and why this is not an error of documentation or scope of GitHub API permissions but complexity of a product.

Code in question

https://github.com/cli/cli/blob/89cbcfe7eb186ff4edbe10792d17bdc55b04f297/pkg/cmd/pr/create/create.go#L554-L568

In the above snippet, gh pr create is pulling information about the repository in order to figure out how to create the PR including branch information, which fails when using the fine-grained PAT you described:

$ GH_DEBUG=api gh pr create
[git remote -v]
[git config --get-regexp ^remote\..*\.gh-resolved$]
* Request at 2024-08-05 16:47:50.324013 -0400 EDT m=+0.049310626
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
> Content-Length: 391
> Content-Type: application/json; charset=utf-8
> Graphql-Features: merge_queue
> Time-Zone: America/New_York
> User-Agent: GitHub CLI 2.54.0

GraphQL query:
fragment repo on Repository {
    id
    name
    owner { login }
    viewerPermission
    defaultBranchRef {
      name
    }
    isPrivate
  }
  query RepositoryNetwork {
    viewer { login }
    
    repo_000: repository(owner: "andyfeller", name: "test-3") {
      ...repo
      parent {
        ...repo
      }
    }
    
  }
GraphQL variables: null

< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Mon, 05 Aug 2024 20:47:50 GMT
< Github-Authentication-Token-Expiration: 2024-08-12 16:40:34 -0400
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: GitHub.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A292:5F13E:B2632A7:14B3A3F7:66B13A76
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4747
< X-Ratelimit-Reset: 1722892210
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 253
< X-Xss-Protection: 0

{
  "data": {
    "viewer": {
      "login": "andyfeller"
    },
    "repo_000": {
      "id": "R_kgDOKBkG6g",
      "name": "test-3",
      "owner": {
        "login": "andyfeller"
      },
      "viewerPermission": "ADMIN",
      "defaultBranchRef": null,
      "isPrivate": true,
      "parent": null
    }
  },
  "errors": [
    {
      "type": "FORBIDDEN",
      "path": [
        "repo_000",
        "defaultBranchRef"
      ],
      "extensions": {
        "saml_failure": false
      },
      "locations": [
        {
          "line": 7,
          "column": 3
        }
      ],
      "message": "Resource not accessible by personal access token"
    }
  ]
}

* Request took 343.27975ms
GraphQL: Resource not accessible by personal access token (repo_000.defaultBranchRef)

Again, because this command is NOT a simple 1:1 mapping to the GitHub APIs, there is more going on here.

Where to go from here?

Ideally, the GitHub REST and GraphQL APIs would provide information about missing permission scopes, which gh could detect and tell you a gh auth refresh command to run to address the missing permissions. I say this because static documentation stating the scopes needed per gh command would easily become incorrect as the features develop.

However, fine-grained tokens are still in public beta status and there is very little information in API response to detect which scopes are missing.

So the question is how might gh commands, which span multiple API requests, accurately communicate the fine grained token scopes needed?

andyfeller avatar Aug 06 '24 11:08 andyfeller

@andyfeller Thanks very much for taking the time to explain the actual nature of the issue here. It's much appreciated. Now you mention it, of course, the gh pr create command is not going to be a 1:1 map to the underlying API calls but combines them together for ease (the whole point of gh!).

Re: where to go from here, I suppose the ideal scenario would be that the exact missing permissions in your fine-grained PAT are specified in the response/error message (which I think is loosely related to #7978).

I realise this might be a way off, so perhaps as an intermediary step, it could be useful to have more clarity in the response about which API call failed. This might already be detectable from the debug logs? Apologies if so - I'm not super familiar with GraphQL!

Happy to provide more feedback/suggestions if it helps.

jjacobgreen avatar Aug 06 '24 12:08 jjacobgreen

I realise this might be a way off, so perhaps as an intermediary step, it could be useful to have more clarity in the response about which API call failed.

This is a good question. πŸ€”

There are ways that we try to inspect the responses from the GitHub API, seeing if there are particular errors we can present an action to the user to fix it. For example, if we can detect this API called because of v1 coarse grain PAT permission scopes. Beyond that, every level bubbles the error up until we eventually exit, determining what type of exit code to return relative to the error.

The problem of writing maintainable code is the higher up the stack you go, the less that code should make about how the job is being done.

Take the actual logic querying the API for repository info:

https://github.com/cli/cli/blob/86964d8809bed7bfbd798d444d5923b8a2bbd4d6/api/queries_repo.go#L281-L332

The errors being returned here could be wrapped and say something like "Unable to retrieve repository info" but it's the error the HTTP client returned that actually has useful information to troubleshoot.

This is also just 1 situation where errors are bubbled up, which is idiomatic in Go. Meaning this would be a massive eating an elephant exercise to go through the whole code to improve error responses.

Without more information from the GitHub APIs regarding scopes, I don't know if there is a good middle ground solution. 😞

andyfeller avatar Aug 07 '24 20:08 andyfeller

@jjacobgreen : Let me be clear: I think you're touching on a few important concerns:

  1. GitHub users want to adopt more secure, tightly scoped API tokens
  2. GitHub CLI users will benefit from having the necessary fine grained permissions documented
  3. GitHub CLI does not get enough information back from GitHub API to detect missing permission(s) to guide users to take action

Given the several issues raised around fine grained v2 PAT use, here's what I suggest:

  • [x] Create an issue to document fine grain PAT permissions per command
  • [x] Follow up with API team regarding V2 PATs and detecting missing permission scopes
  • [ ] Based on ☝ create issue(s) to ensure GitHub CLI is capable of detecting and presenting this info

andyfeller avatar Aug 14 '24 12:08 andyfeller

For completeness, this is the logic involved in detecting incorrect token permission scopes when 4XX HTTP errors are encountered, reading information out of X-Oauth-Scopes and X-Accepted-Oauth-Scopes to help users:

https://github.com/cli/cli/blob/9e27af999ebd7e776ba5c2373371ed59a6eda096/api/client.go#L159-L257

As we see in the debug data above, that information isn't readily available when using fine grained PATs. πŸ€”

andyfeller avatar Aug 14 '24 13:08 andyfeller

Created https://github.com/cli/cli/issues/9461 to capture which fine grain PAT permissions are needed to use GitHub CLI.

andyfeller avatar Aug 14 '24 15:08 andyfeller

@jjacobgreen : After discussing with the Authorization team, it appears GraphQL doesn't readily have information about the necessary fine grain PAT scopes by object and mutation, making it impossible for gh to guide the user on how to solve this issue.

For now, I want to use this issue to track improving gh ability to detect missing fine grain PAT scopes despite being blocked by missing API capability.

andyfeller avatar Aug 20 '24 18:08 andyfeller

@andyfeller sorry for the delayed response. I appreciate the attention that you've been giving to this. Sounds like a good plan. Let me know if there's any helpful input I can provide.

jjacobgreen avatar Aug 21 '24 10:08 jjacobgreen

The permissions are also a bit surprising. While I can't query defaultBranchRef via GraphQL (GraphQL: Resource not accessible by integration (repository.defaultBranchRef), it works fine by REST API (using https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-metadata).

MartinNowak avatar Oct 06 '25 14:10 MartinNowak