|
| 1 | +================== |
| 2 | +GitHub Application |
| 3 | +================== |
| 4 | + |
| 5 | +GitHub offers two different ways to access users' data from its own platform: |
| 6 | +OAuth Application and GitHub Application. Depending on the applications |
| 7 | +needings, GitHub recommends using one or the other. |
| 8 | + |
| 9 | +Read the Docs currently uses the OAuth Application to support "Login with |
| 10 | +GitHub" and "Import Project". They require different level of permissions, but |
| 11 | +OAuth Application does not really allow us to manage them in a granular way. |
| 12 | + |
| 13 | +Based in our needings and the GitHub recommendations in |
| 14 | +https://docs.github.com/en/developers/apps/getting-started-with-apps/about-apps#determining-which-integration-to-build |
| 15 | +we should use a GitHub Application for Read the Docs:: |
| 16 | + |
| 17 | + Only as me? -> NO |
| 18 | + Act as an App -> NO |
| 19 | + Access Everything? -> NO |
| 20 | + -> GitHub Application |
| 21 | + |
| 22 | +Or in case we need to act as an App, we end in the same route:: |
| 23 | + |
| 24 | + Only as me? -> NO |
| 25 | + Act as an App -> YES |
| 26 | + -> GitHub Application |
| 27 | + |
| 28 | +This document explains some problems we have found in our current implementation |
| 29 | +using OAuth Application and explore the recommended way using GitHub |
| 30 | +Application. It goes through the work required and touches a little about its |
| 31 | +implementation. |
| 32 | + |
| 33 | + |
| 34 | +Problems with current implementation |
| 35 | +------------------------------------ |
| 36 | + |
| 37 | +* We request too permisives scope at Sign Up |
| 38 | + |
| 39 | + Even if our application will never require some of these scopes (because it's |
| 40 | + a read user --and will never import a project), we ask for too many permissions. |
| 41 | + |
| 42 | +* OAuth Application does not provide granular permissions |
| 43 | + |
| 44 | + We have to ask for ``read`` scope. It gives us read & write permissions to |
| 45 | + *all the repositories of this user*. This makes some users/organizations to |
| 46 | + don't want to user our platform. |
| 47 | + |
| 48 | + Besides, ``admin:repo_hook`` scope is also required to be able to create a |
| 49 | + webhook for pushes and trigger a new build. |
| 50 | + |
| 51 | +* Repositories from an GitHub Organization does not appear in the "Import |
| 52 | + Project" list |
| 53 | + |
| 54 | + Organizations with OAuth App access restriction enabled, require users using |
| 55 | + our OAuth Application to request permissions to the GitHub Organization's |
| 56 | + owner to accept our OAuth App. |
| 57 | + |
| 58 | + |
| 59 | +Goals |
| 60 | +----- |
| 61 | + |
| 62 | +* Reduce as much as possible granted permissions for our GitHub Application |
| 63 | +* Do not require access to *all the users'/organizations' repositories* |
| 64 | +* Allow Read the Docs GitHub Application only to specific repositories |
| 65 | +* Support GitHub Application *and* OAuth Application at the same time during the |
| 66 | + migration time |
| 67 | +* Support all the GitHub features current using OAuth Application |
| 68 | + * GitHub SSO |
| 69 | + * Pull Request builder |
| 70 | + * Trigger a build on push |
| 71 | +* Don't require SSH key to clone private repositories |
| 72 | + |
| 73 | + |
| 74 | +Non Goals |
| 75 | +--------- |
| 76 | + |
| 77 | +* Escalate permissions as they are needed |
| 78 | +* Migrate all current OAuth Application users to GitHub Application users at |
| 79 | + once |
| 80 | +* Implement something similar to GitHub Application for other VCS providers |
| 81 | + (GitLab and Bitbucket) |
| 82 | + |
| 83 | + |
| 84 | +Context related to GitHub Application |
| 85 | +------------------------------------- |
| 86 | + |
| 87 | +Considering there are too much confusion around GitHub Application and OAuth |
| 88 | +Application, and copy&paste some context from its official documentation trying |
| 89 | +to mitigate it and have a better understanding of this design doc. |
| 90 | + |
| 91 | + GitHub Apps are first-class actors within GitHub. A GitHub App acts on its own |
| 92 | + behalf, taking actions via the API directly using its own identity, which |
| 93 | + means you don't need to maintain a bot or service account as a separate user. |
| 94 | + |
| 95 | + GitHub Apps can be installed directly on organizations and user accounts and |
| 96 | + granted access to specific repositories. They come with built-in webhooks and |
| 97 | + narrow, specific permissions. When you set up your GitHub App, you can select |
| 98 | + the repositories you want it to access |
| 99 | + |
| 100 | + To install a GitHub App, you must be an organization owner or have admin |
| 101 | + permissions in a repository. |
| 102 | + |
| 103 | + Don't build an OAuth App if you want your application to act on a single |
| 104 | + repository. With the repo OAuth scope, OAuth Apps can act on all of the |
| 105 | + authenticated user's repositories. |
| 106 | + |
| 107 | +(from https://docs.github.com/en/developers/apps/getting-started-with-apps/about-apps) |
| 108 | + |
| 109 | + You can install GitHub Apps in your personal account or organizations you own. |
| 110 | + If you have admin permissions in a repository, you can install GitHub Apps on |
| 111 | + organization accounts. If a GitHub App is installed in a repository and requires |
| 112 | + organization permissions, the organization owner must approve the application. |
| 113 | + |
| 114 | + Account owners can use a GitHub App in one account without granting access to |
| 115 | + another ... A GitHub App remains installed if the person who set it up leaves |
| 116 | + the organization. |
| 117 | + |
| 118 | + The installation token from a GitHub App loses access to resources if an admin |
| 119 | + removes repositories from the installation. |
| 120 | + |
| 121 | + Installation access tokens are limited to specified repositories with the |
| 122 | + permissions chosen by the creator of the app. |
| 123 | + |
| 124 | + GitHub Apps can request separate access to issues and pull requests without |
| 125 | + accessing the actual contents of the repository. |
| 126 | + |
| 127 | + A GitHub App receives a webhook event when an installation is changed or |
| 128 | + removed. This tells the app creator when they've received more or less access to |
| 129 | + an organization's resources. |
| 130 | + |
| 131 | + A GitHub App can request an installation access token by using a private key |
| 132 | + with a JSON web token format out-of-band. |
| 133 | + |
| 134 | + An installation token identifies the app as the GitHub Apps bot, such as |
| 135 | + @jenkins-bot. |
| 136 | + |
| 137 | + GitHub Apps can authenticate on behalf of the user, which is called |
| 138 | + user-to-server requests. The flow to authorize is the same as the OAuth App |
| 139 | + authorization flow. User-to-server tokens can expire and be renewed with a |
| 140 | + refresh token |
| 141 | + |
| 142 | +(from https://docs.github.com/en/developers/apps/getting-started-with-apps/differences-between-github-apps-and-oauth-apps) |
| 143 | + |
| 144 | + |
| 145 | +GitHub Application requirements |
| 146 | +------------------------------- |
| 147 | + |
| 148 | +:Repository permissions: |
| 149 | + - Metadata: Read-only (mandatory) |
| 150 | + - Contents: Read-only |
| 151 | + - Commit statuses: Read & Write |
| 152 | + - Pull requests: Read-only |
| 153 | + |
| 154 | +:Organization permissions: |
| 155 | + - Members: Read-only |
| 156 | + |
| 157 | +:User permissions: |
| 158 | + - Email addresses: Read-only |
| 159 | + |
| 160 | +:Events subscribed: |
| 161 | + - Meta (When this App is deleted and the associated hook is removed) |
| 162 | + - Create (Branch or tag created) |
| 163 | + - Delete (Branch or tag deleted) |
| 164 | + - Push (Git push to a repository) |
| 165 | + - Pull request (Pull request opened, closed, reopened, ...) |
| 166 | + |
| 167 | + |
| 168 | +Authentication as GitHub Application |
| 169 | +------------------------------------ |
| 170 | + |
| 171 | +https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps |
| 172 | + |
| 173 | +Generate the JWT on Python: |
| 174 | + |
| 175 | +.. code:: python |
| 176 | +
|
| 177 | + import datetime |
| 178 | + import jwt |
| 179 | + GITHUB_APP_ID = 134302 |
| 180 | +
|
| 181 | + # content of PEM file downloaded from GH Application settings |
| 182 | + private_key = b'''-----BEGIN RSA PRIVATE KEY-----''' |
| 183 | +
|
| 184 | + jwt.encode({"iat": datetime.datetime.utcnow() - datetime.timedelta(seconds=60), "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=5), "iss": GITHUB_APP_ID}, private_key, algorithm="RS256") |
| 185 | +
|
| 186 | +Use the token generated in the previous step as authorization in the cURL command: |
| 187 | + |
| 188 | +.. code:: bash |
| 189 | +
|
| 190 | + $ curl -H "Authorization: Bearer <JWT>" -H "Accept: application/vnd.github.v3+json" https://api.github.com/app |
| 191 | +
|
| 192 | +
|
| 193 | +.. note:: |
| 194 | + |
| 195 | + For some reason this is not working and I'm getting: |
| 196 | + |
| 197 | + .. code:: text |
| 198 | +
|
| 199 | + 'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued |
| 200 | +
|
| 201 | +
|
| 202 | +SSH keys are not required to clone private repositories |
| 203 | +------------------------------------------------------- |
| 204 | + |
| 205 | +When asking for ``contents`` (repository permission) we are able to clone |
| 206 | +private repositories by using GitHub Application installation access tokens: |
| 207 | + |
| 208 | +.. code:: bash |
| 209 | +
|
| 210 | + git clone https://x-access-token:<token>@github.com/owner/repo.git |
| 211 | +
|
| 212 | +(from |
| 213 | +https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#http-based-git-access-by-an-installation) |
| 214 | + |
| 215 | + |
| 216 | +Handling webhooks |
| 217 | +----------------- |
| 218 | + |
| 219 | +GitHub Application has *only one webhook* where it will receive all the events |
| 220 | +for all the installations. The body contains all the information about the |
| 221 | +particular installation that triggered the event. With this data, we will create |
| 222 | +an access token and perform the query/actions we need. |
| 223 | + |
| 224 | +There are some events that need to map to particular ``Project`` in our |
| 225 | +database. For example, "a push to ``main`` branch in repository |
| 226 | +``readthedocs/blog``" should "trigger a build for ``latest`` version on |
| 227 | +``read-the-docs-blog`` project". For these cases we can use use |
| 228 | +``repository.id`` field from the body to get the ``RemoteRepository.remote_id`` |
| 229 | +and from there get the ``Project``. |
| 230 | + |
| 231 | + |
| 232 | +Remote* models re-sync |
| 233 | +---------------------- |
| 234 | + |
| 235 | +Currently, we are using 2 endpoints to sync all the ``Remote*`` models: |
| 236 | + |
| 237 | +* ``https://api.github.com/user/repos`` (https://docs.github.com/en/rest/reference/repos#list-repositories-for-the-authenticated-user) |
| 238 | +* ``https://api.github.com/org/{org}/repos`` (https://docs.github.com/en/rest/reference/repos#list-organization-repositories) |
| 239 | + |
| 240 | +However, this endpoints won't return the same data when using GitHub Application |
| 241 | +since we won't be authenticated as a user anymore and the ``permission.admin: |
| 242 | +boolean`` field won't come in the response. |
| 243 | + |
| 244 | +Instead, we will have to iterate over, |
| 245 | + |
| 246 | +* all repositories accessible to the app installation |
| 247 | + |
| 248 | + * ``https://api.github.com/installation/repositories`` (https://docs.github.com/en/rest/reference/apps#list-repositories-accessible-to-the-app-installation) |
| 249 | +* iterate over each repository checking for user's permission |
| 250 | + |
| 251 | + * ``https://api.github.com/repos/{owner}/{repo}/collaborators/{username}/permission`` (https://docs.github.com/en/rest/reference/repos#get-repository-permissions-for-a-user) |
| 252 | + |
| 253 | +By doing this, we will keep our ``Remote*`` table very small because we will |
| 254 | +only track repositories that users gave us permissions. Then, only these |
| 255 | +repositories will be shown in the "Import Project" page. |
| 256 | + |
| 257 | +Once they created a new repository under their organization, they will need to |
| 258 | +go to the GitHub Application installation configuration and grant access to the |
| 259 | +new repository. This will send us a webhook (``installation_repositories``, |
| 260 | +https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#installation_repositories) |
| 261 | +and we can automatically do a re-sync of ``Remote*`` models. |
| 262 | + |
| 263 | +.. note:: |
| 264 | + |
| 265 | + Users installing our GitHub Application and selecting "All repositories" will |
| 266 | + always see all the repositories on the "Import Project" page. |
| 267 | + |
| 268 | + |
| 269 | +Need more research |
| 270 | +------------------ |
| 271 | + |
| 272 | +* Does django-allauth support GitHub Application for Login? |
| 273 | + |
| 274 | + Using the Client ID for the GitHub Application (instead the OAuth |
| 275 | + application), should make it to work: |
| 276 | + https://docs.github.com/en/developers/apps/building-github-apps/identifying-and-authorizing-users-for-github-apps#web-application-flow |
| 277 | + |
| 278 | +* How do we use Client ID for GitHub Application for new users and Client ID for |
| 279 | + OAuth application for current/existing users? |
| 280 | + |
| 281 | +* Should we keep using our OAuth Application for login and GitHub Application |
| 282 | + for the rest? Is it possible? |
| 283 | + |
| 284 | + Related: https://docs.github.com/en/developers/apps/getting-started-with-apps/migrating-oauth-apps-to-github-apps |
| 285 | + Related: https://github.com/readthedocs/readthedocs-ops/issues/532 |
| 286 | + |
| 287 | + |
| 288 | +Questions |
| 289 | +--------- |
| 290 | + |
| 291 | +* How are we going to handle other VCS providers (GitLab and Bitbucket)? |
| 292 | + |
| 293 | + GitLab and Bitbucket does not offer another option than OAuth Application. We |
| 294 | + need to maintain the implementation that we currently have for them. |
| 295 | + |
| 296 | +* Does it worth the effort integrating GitHub Application without being able to |
| 297 | + use the same for other services? |
| 298 | + |
| 299 | + GitHub is the most VCS provider used in our platform. Because of this, if the |
| 300 | + we can provide a better UX to most of our users I'd call it a win. |
| 301 | + |
| 302 | +* Do we need to support both, GitHub Application and GitHub OAuth, at the same time? |
| 303 | + |
| 304 | + Yes. Current users will keep using GitHub OAuth for a long period of time. We |
| 305 | + could notify them encouraging them to migrate to our new GitHub Application. |
| 306 | + However, this will take a non-trivial amount of time. |
0 commit comments