jupyterhub / gh-scoped-creds

Provide fine-grained push access to GitHub from a JupyterHub
BSD 3-Clause "New" or "Revised" License
25 stars 7 forks source link

Works in the notebook/console! Do you want a PR? :) #7

Closed fperez closed 2 years ago

fperez commented 2 years ago

Try running this version in a notebook:


import argparse
import requests
import sys
import time
import os

from IPython.display import display, Javascript
import ipywidgets as widgets

def do_authenticate_device_flow(client_id):
    """
    Authenticate user with given GitHub app using GitHub OAuth Device flow
    https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#device-flow
    describes what happens here.
    Returns an access_code and the number of seconds it expires in.
    access_code will have scopes defined in the GitHub app
    """
    verification_resp = requests.post(
        "https://github.com/login/device/code",
        data={"client_id": client_id, "scope": "repo"},
        headers={"Accept": "application/json"},
    ).json()

    url  = verification_resp["verification_uri"]
    code = verification_resp["user_code"]

    display(Javascript(f'navigator.clipboard.writeText("{code}");'))

    print(f'The code {code} has been copied to your clipboard.')
    print(f'You have 15 minutes to go to this URL and paste it there:')
    print(f'{url}')

    ans = input("Hit ENTER to open that page in a new tab (type anything to cancel)>")
    if ans:
        print("Automatic opening canceled!")
    else:
        display(Javascript(f'window.open("{url}", "_blank");'))

    print('Waiting...', end='', flush=True)

    while True:
        time.sleep(verification_resp["interval"])
        print('.', end='', flush=True)
        access_resp = requests.post(
            "https://github.com/login/oauth/access_token",
            data={
                "client_id": client_id,
                "device_code": verification_resp["device_code"],
                "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            },
            headers={"Accept": "application/json"},
        ).json()
        if "access_token" in access_resp:
            print()
            return access_resp["access_token"], access_resp["expires_in"]

def main():

    client_id = os.environ.get("GITHUB_APP_CLIENT_ID")
    git_credentials_path = "/tmp/github-app-git-credentials" 

    access_token, expires_in = do_authenticate_device_flow(client_id)
    expires_in_hours = expires_in / 60 / 60
    print(f"\nSuccess! Authentication will expire in {expires_in_hours:0.1f} hours.")

    # Create the file with appropriate permissions (0600) so other users can't read it
    with open(os.open(git_credentials_path, os.O_WRONLY | os.O_CREAT, 0o600), "w") as f:
        f.write(f"https://x-access-token:{access_token}@github.com\n")

and then just call main() either in the notebook or a console attached to it. I think works really nicely!

image

Do you want a PR for this? Or you can go ahead and do it :)

I'd refactor the code a bit to reuse more at the cmd line and Jupyter, and I'd add a magic, say %ghauth, to shorten this. We can then add the magic to __init__ and pre-import it, or even add it to a button/menu on the UI that shows the current GH connection status and lets you refresh the auth by clicking on it...

But anyway, I think this is now good enough for everyday use, esp. if we add a magic we preload or similar for convenience....

fperez commented 2 years ago

With this workflow, it's just running one command, enter, paste, enter, click on the GH green authorize button, close tab, done. I kind of like it :)

fperez commented 2 years ago

BTW - I'm going to make a PR against IPython adding this new magic:

from IPython.core.magic import register_line_magic

@register_line_magic
def pym(line):
    "Equivalent to 'python -m'"

    import runpy
    runpy.run_module(line)

which then gives us this clean workflow (my ghauth module is the same as yours, just with the above JS code and a shorter name for convenience):

image

The idea here is that, once we put %pym into IPython, then any python package/module that offers a python -m entry point can become a cmd line magic as %pym pkg! And we can then expose nice Jupyter-oriented functionality in packages without the need for them to explicitly register magics or have users import anything :)

yuvipanda commented 2 years ago

@fperez wow this is awesome! PR would be lovely - especially if we can make the Javascript calls conditional on us running in a notebook, so this could continue to run in terminals still. HPC users might need that still.

fperez commented 2 years ago

Yup! I haven't moved forward with the PR precisely b/c I'm thinking of how to make it as smooth as possible for all use cases, terminal and notebook, without introducing user-facing annoyances... I think I know how to proceed, I just need to block a couple of hours to do a bit of refactoring on your code and add a clean version of the above. But having this scratch proof of concept helps :)

fperez commented 2 years ago

Hey, I found a better solution! It turns out that %run already provides a -m flag! I'll make a quick PR now, and we can discuss there :)

yuvipanda commented 2 years ago

This was fixed up in #8!