Ejemplo n.º 1
0
async def test_paginated_teams(client: AsyncClient,
                               respx_mock: respx.Router) -> None:
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[
            GitHubTeam(slug="a-team", gid=1000, organization="org"),
            GitHubTeam(slug="other-team", gid=1001, organization="org"),
            GitHubTeam(slug="third-team", gid=1002, organization="foo"),
            GitHubTeam(
                slug="team-with-very-long-name",
                gid=1003,
                organization="other-org",
            ),
        ],
    )

    r = await simulate_github_login(client,
                                    respx_mock,
                                    user_info,
                                    paginate_teams=True)
    assert r.status_code == 307

    # Check the group list.
    r = await client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    expected = ",".join([
        "org-a-team",
        "org-other-team",
        "foo-third-team",
        "other-org-team-with-very--F279yg",
    ])
    assert r.headers["X-Auth-Request-Groups"] == expected
Ejemplo n.º 2
0
async def test_login_redirect_header(client: AsyncClient,
                                     respx_mock: respx.Router) -> None:
    """Test receiving the redirect header via X-Auth-Request-Redirect."""
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")],
    )
    return_url = "https://example.com/foo?a=bar&b=baz"
    await mock_github(respx_mock, "some-code", user_info)

    # Simulate the initial authentication request.
    r = await client.get("/login",
                         headers={"X-Auth-Request-Redirect": return_url})
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    query = parse_qs(url.query)

    # Simulate the return from GitHub.
    r = await client.get("/login",
                         params={
                             "code": "some-code",
                             "state": query["state"][0]
                         })
    assert r.status_code == 307
    assert r.headers["Location"] == return_url
Ejemplo n.º 3
0
async def test_unicode_name(client: AsyncClient,
                            respx_mock: respx.Router) -> None:
    user_info = GitHubUserInfo(
        name="名字",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")],
    )

    r = await simulate_github_login(client, respx_mock, user_info)
    assert r.status_code == 307

    # Check that the name as returned from the user-info API is correct.
    r = await client.get("/auth/api/v1/user-info")
    assert r.status_code == 200
    assert r.json() == {
        "username": "******",
        "name": "名字",
        "email": "*****@*****.**",
        "uid": 123456,
        "groups": [{
            "name": "org-a-team",
            "id": 1000
        }],
    }
Ejemplo n.º 4
0
async def test_bad_redirect(client: AsyncClient,
                            respx_mock: respx.Router) -> None:
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")],
    )

    r = await client.get("/login", params={"rd": "https://foo.example.com/"})
    assert r.status_code == 422

    r = await client.get(
        "/login",
        headers={"X-Auth-Request-Redirect": "https://foo.example.com/"},
    )
    assert r.status_code == 422

    # But if we're deployed under foo.example.com as determined by the
    # X-Forwarded-Host header, this will be allowed.
    r = await simulate_github_login(
        client,
        respx_mock,
        user_info,
        headers={
            "X-Forwarded-For": "192.168.0.1",
            "X-Forwarded-Host": "foo.example.com",
        },
        return_url="https://foo.example.com/",
    )
    assert r.status_code == 307
Ejemplo n.º 5
0
async def test_cookie_auth_with_token(client: AsyncClient,
                                      respx_mock: respx.Router) -> None:
    """Test that cookie auth takes precedence over an Authorization header.

    JupyterHub sends an Authorization header in its internal requests with
    type token.  We want to ensure that we prefer our session cookie, rather
    than try to unsuccessfully parse that header.  Test this by completing a
    login to get a valid session and then make a request with a bogus
    Authorization header.
    """
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")],
    )

    # Simulate the GitHub login.
    r = await simulate_github_login(
        client,
        respx_mock,
        user_info,
        headers={"Authorization": "token some-jupyterhub-token"},
    )

    # Now make a request to the /auth endpoint with a bogus token.
    r = await client.get(
        "/auth",
        params={"scope": "read:all"},
        headers={"Authorization": "token some-jupyterhub-token"},
    )
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-User"] == "githubuser"
Ejemplo n.º 6
0
async def test_logout_github(
    client: AsyncClient,
    config: Config,
    respx_mock: respx.Router,
    caplog: LogCaptureFixture,
) -> None:
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[
            GitHubTeam(slug="a-team", gid=1000, organization="org"),
        ],
    )

    # Log in and log out.
    await mock_github(respx_mock, "some-code", user_info, expect_revoke=True)
    r = await client.get("/login", params={"rd": "https://example.com"})
    assert r.status_code == 307
    query = query_from_url(r.headers["Location"])
    r = await client.get("/login",
                         params={
                             "code": "some-code",
                             "state": query["state"][0]
                         })
    assert r.status_code == 307
    caplog.clear()
    r = await client.get("/logout")

    # Check the redirect and logging.
    assert r.status_code == 307
    assert r.headers["Location"] == config.after_logout_url
    assert parse_log(caplog) == [
        {
            "event": "Revoked GitHub OAuth authorization",
            "httpRequest": {
                "requestMethod": "GET",
                "requestUrl": f"https://{TEST_HOSTNAME}/logout",
                "remoteIp": "127.0.0.1",
            },
            "severity": "info",
        },
        {
            "event": "Successful logout",
            "httpRequest": {
                "requestMethod": "GET",
                "requestUrl": f"https://{TEST_HOSTNAME}/logout",
                "remoteIp": "127.0.0.1",
            },
            "severity": "info",
        },
    ]
Ejemplo n.º 7
0
async def test_invalid_groups(client: AsyncClient,
                              respx_mock: respx.Router) -> None:
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[
            GitHubTeam(slug="a-team", gid=1000, organization="ORG"),
            GitHubTeam(slug="broken slug", gid=4000, organization="ORG"),
            GitHubTeam(slug="valid", gid=5000, organization="bad:org"),
        ],
    )

    r = await simulate_github_login(client, respx_mock, user_info)
    assert r.status_code == 307

    # The invalid groups should not appear but the valid group should still be
    # present.
    r = await client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-Groups"] == "org-a-team"
Ejemplo n.º 8
0
async def test_invalid_groups(setup: SetupTest) -> None:
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[
            GitHubTeam(slug="a-team", gid=1000, organization="ORG"),
            GitHubTeam(slug="broken slug", gid=4000, organization="ORG"),
            GitHubTeam(slug="valid", gid=5000, organization="bad:org"),
        ],
    )

    setup.set_github_token_response("some-code", "some-github-token")
    r = await setup.client.get(
        "/login",
        headers={"X-Auth-Request-Redirect": "https://example.com"},
        allow_redirects=False,
    )
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    query = parse_qs(url.query)

    # Simulate the return from GitHub.
    setup.set_github_userinfo_response("some-github-token", user_info)
    r = await setup.client.get(
        "/login",
        params={
            "code": "some-code",
            "state": query["state"][0]
        },
        allow_redirects=False,
    )
    assert r.status_code == 307

    # The user returned by the /auth route should be forced to lowercase.
    r = await setup.client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-Groups"] == "org-a-team"
Ejemplo n.º 9
0
async def test_cookie_auth_with_token(setup: SetupTest) -> None:
    """Test that cookie auth takes precedence over an Authorization header.

    JupyterHub sends an Authorization header in its internal requests with
    type token.  We want to ensure that we prefer our session cookie, rather
    than try to unsuccessfully parse that header.  Test this by completing a
    login to get a valid session and then make a request with a bogus
    Authorization header.
    """
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")],
    )

    setup.set_github_token_response("some-code", "some-github-token")
    r = await setup.client.get(
        "/login",
        params={"rd": "https://example.com/foo"},
        headers={"Authorization": "token some-jupyterhub-token"},
        allow_redirects=False,
    )
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    query = parse_qs(url.query)

    # Simulate the return from GitHub.
    setup.set_github_userinfo_response("some-github-token", user_info)
    r = await setup.client.get(
        "/login",
        params={
            "code": "some-code",
            "state": query["state"][0]
        },
        headers={"Authorization": "token some-jupyterhub-token"},
        allow_redirects=False,
    )
    assert r.status_code == 307
    assert r.headers["Location"] == "https://example.com/foo"

    # Now make a request to the /auth endpoint with a bogus token.
    r = await setup.client.get(
        "/auth",
        params={"scope": "read:all"},
        headers={"Authorization": "token some-jupyterhub-token"},
    )
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-User"] == "githubuser"
Ejemplo n.º 10
0
async def test_invalid_username(client: AsyncClient,
                                respx_mock: respx.Router) -> None:
    """Test that invalid usernames are rejected."""
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")],
    )

    r = await simulate_github_login(client,
                                    respx_mock,
                                    user_info,
                                    expect_revoke=True)
    assert r.status_code == 403
    assert "Invalid username: invalid user" in r.text
Ejemplo n.º 11
0
async def test_github_uppercase(setup: SetupTest) -> None:
    """Tests that usernames and organization names are forced to lowercase.

    We do not test that slugs are forced to lowercase (and do not change the
    case of slugs) because GitHub should already be coercing lowercase when
    creating the slug.
    """
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")],
    )

    setup.set_github_token_response("some-code", "some-github-token")
    r = await setup.client.get(
        "/login",
        headers={"X-Auth-Request-Redirect": "https://example.com"},
        allow_redirects=False,
    )
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    query = parse_qs(url.query)

    # Simulate the return from GitHub.
    setup.set_github_userinfo_response("some-github-token", user_info)
    r = await setup.client.get(
        "/login",
        params={
            "code": "some-code",
            "state": query["state"][0]
        },
        allow_redirects=False,
    )
    assert r.status_code == 307

    # The user returned by the /auth route should be forced to lowercase.
    r = await setup.client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-User"] == "someuser"
Ejemplo n.º 12
0
async def test_github_admin(client: AsyncClient, respx_mock: respx.Router,
                            factory: ComponentFactory) -> None:
    """Test that a token administrator gets the admin:token scope."""
    admin_service = factory.create_admin_service()
    async with factory.session.begin():
        await admin_service.add_admin("someuser",
                                      actor="admin",
                                      ip_address="127.0.0.1")
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")],
    )

    r = await simulate_github_login(client, respx_mock, user_info)
    assert r.status_code == 307

    # The user should have admin:token scope.
    r = await client.get("/auth", params={"scope": "admin:token"})
    assert r.status_code == 200
Ejemplo n.º 13
0
async def test_claim_names(setup: SetupTest) -> None:
    """Uses an alternate settings environment with non-default claims."""
    setup.configure(username_claim="username", uid_claim="numeric-uid")
    assert setup.config.github
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="org")],
    )

    setup.set_github_token_response("some-code", "some-github-token")
    r = await setup.client.get(
        "/login",
        headers={"X-Auth-Request-Redirect": "https://example.com"},
        allow_redirects=False,
    )
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    query = parse_qs(url.query)

    # Simulate the return from GitHub.
    setup.set_github_userinfo_response("some-github-token", user_info)
    r = await setup.client.get(
        "/login",
        params={
            "code": "some-code",
            "state": query["state"][0]
        },
        allow_redirects=False,
    )
    assert r.status_code == 307

    # Check that the /auth route works and sets the headers correctly.
    r = await setup.client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-User"] == "githubuser"
    assert r.headers["X-Auth-Request-Uid"] == "123456"
Ejemplo n.º 14
0
async def test_github_uppercase(client: AsyncClient,
                                respx_mock: respx.Router) -> None:
    """Tests that usernames and organization names are forced to lowercase.

    We do not test that slugs are forced to lowercase (and do not change the
    case of slugs) because GitHub should already be coercing lowercase when
    creating the slug.
    """
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")],
    )

    r = await simulate_github_login(client, respx_mock, user_info)
    assert r.status_code == 307

    # The user returned by the /auth route should be forced to lowercase.
    r = await client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-User"] == "someuser"
Ejemplo n.º 15
0
async def test_github_admin(setup: SetupTest) -> None:
    """Test that a token administrator gets the admin:token scope."""
    admin_service = setup.factory.create_admin_service()
    admin_service.add_admin("someuser", actor="admin", ip_address="127.0.0.1")
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")],
    )

    setup.set_github_token_response("some-code", "some-github-token")
    r = await setup.client.get(
        "/login",
        headers={"X-Auth-Request-Redirect": "https://example.com"},
        allow_redirects=False,
    )
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    query = parse_qs(url.query)

    # Simulate the return from GitHub.
    setup.set_github_userinfo_response("some-github-token", user_info)
    r = await setup.client.get(
        "/login",
        params={
            "code": "some-code",
            "state": query["state"][0]
        },
        allow_redirects=False,
    )
    assert r.status_code == 307

    # The user should have admin:token scope.
    r = await setup.client.get("/auth", params={"scope": "admin:token"})
    assert r.status_code == 200
Ejemplo n.º 16
0
async def test_login(setup: SetupTest, caplog: LogCaptureFixture) -> None:
    assert setup.config.github
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[
            GitHubTeam(slug="a-team", gid=1000, organization="org"),
            GitHubTeam(slug="other-team", gid=1001, organization="org"),
            GitHubTeam(
                slug="team-with-very-long-name",
                gid=1002,
                organization="other-org",
            ),
        ],
    )
    return_url = "https://example.com:4444/foo?a=bar&b=baz"

    # Simulate the initial authentication request.
    setup.set_github_token_response("some-code", "some-github-token")
    r = await setup.client.get("/login",
                               params={"rd": return_url},
                               allow_redirects=False)
    assert r.status_code == 307
    url = urlparse(r.headers["Location"])
    assert url.scheme == "https"
    assert "github.com" in url.netloc
    assert url.query
    query = parse_qs(url.query)
    assert query == {
        "client_id": [setup.config.github.client_id],
        "scope": [" ".join(GitHubProvider._SCOPES)],
        "state": [ANY],
    }
    data = json.loads(caplog.record_tuples[-1][2])
    assert data == {
        "event": "Redirecting user to GitHub for authentication",
        "level": "info",
        "logger": "gafaelfawr",
        "method": "GET",
        "path": "/login",
        "return_url": return_url,
        "remote": "127.0.0.1",
        "request_id": ANY,
        "user_agent": ANY,
    }

    # Simulate the return from GitHub.
    caplog.clear()
    setup.set_github_userinfo_response("some-github-token", user_info)
    r = await setup.client.get(
        "/login",
        params={
            "code": "some-code",
            "state": query["state"][0]
        },
        allow_redirects=False,
    )
    assert r.status_code == 307
    assert r.headers["Location"] == return_url
    data = json.loads(caplog.record_tuples[-1][2])
    assert data == {
        "event": "Successfully authenticated user githubuser (123456)",
        "level": "info",
        "logger": "gafaelfawr",
        "method": "GET",
        "path": "/login",
        "return_url": return_url,
        "remote": "127.0.0.1",
        "request_id": ANY,
        "scope": "read:all user:token",
        "token": ANY,
        "user": "******",
        "user_agent": ANY,
    }

    # Check that the /auth route works and finds our token.
    r = await setup.client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-Token-Scopes"] == "read:all user:token"
    assert r.headers["X-Auth-Request-Scopes-Accepted"] == "read:all"
    assert r.headers["X-Auth-Request-Scopes-Satisfy"] == "all"
    assert r.headers["X-Auth-Request-User"] == "githubuser"
    assert r.headers["X-Auth-Request-Name"] == "GitHub User"
    assert r.headers["X-Auth-Request-Email"] == "*****@*****.**"
    assert r.headers["X-Auth-Request-Uid"] == "123456"
    expected = "org-a-team,org-other-team,other-org-team-with-very--F279yg"
    assert r.headers["X-Auth-Request-Groups"] == expected
Ejemplo n.º 17
0
async def test_login(client: AsyncClient, respx_mock: respx.Router,
                     caplog: LogCaptureFixture) -> None:
    user_info = GitHubUserInfo(
        name="GitHub User",
        username="******",
        uid=123456,
        email="*****@*****.**",
        teams=[
            GitHubTeam(slug="a-team", gid=1000, organization="org"),
            GitHubTeam(slug="other-team", gid=1001, organization="org"),
            GitHubTeam(
                slug="team-with-very-long-name",
                gid=1002,
                organization="other-org",
            ),
        ],
    )
    return_url = "https://example.com:4444/foo?a=bar&b=baz"

    # Simulate the GitHub login.
    caplog.clear()
    r = await simulate_github_login(client,
                                    respx_mock,
                                    user_info,
                                    return_url=return_url)
    assert r.status_code == 307
    assert parse_log(caplog) == [
        {
            "event": "Redirecting user to GitHub for authentication",
            "httpRequest": {
                "requestMethod": "GET",
                "requestUrl": ANY,
                "remoteIp": "127.0.0.1",
            },
            "return_url": return_url,
            "severity": "info",
        },
        {
            "event": "Successfully authenticated user githubuser (123456)",
            "httpRequest": {
                "requestMethod": "GET",
                "requestUrl": ANY,
                "remoteIp": "127.0.0.1",
            },
            "return_url": return_url,
            "scope": "read:all user:token",
            "severity": "info",
            "token": ANY,
            "user": "******",
        },
    ]

    # Examine the resulting cookie and ensure that it has the proper metadata
    # set.
    cookie = next((c for c in r.cookies.jar if c.name == "gafaelfawr"))
    assert cookie.secure
    assert cookie.discard
    assert cookie.has_nonstandard_attr("HttpOnly")
    assert cookie.get_nonstandard_attr("SameSite") == "lax"

    # Check that the /auth route works and finds our token, and that the user
    # information is correct.
    r = await client.get("/auth", params={"scope": "read:all"})
    assert r.status_code == 200
    assert r.headers["X-Auth-Request-Token-Scopes"] == "read:all user:token"
    assert r.headers["X-Auth-Request-Scopes-Accepted"] == "read:all"
    assert r.headers["X-Auth-Request-Scopes-Satisfy"] == "all"
    assert r.headers["X-Auth-Request-User"] == "githubuser"
    assert r.headers["X-Auth-Request-Email"] == "*****@*****.**"
    assert r.headers["X-Auth-Request-Uid"] == "123456"
    expected = "org-a-team,org-other-team,other-org-team-with-very--F279yg"
    assert r.headers["X-Auth-Request-Groups"] == expected

    # Do the same verification with the user-info endpoint.
    r = await client.get("/auth/api/v1/user-info")
    assert r.status_code == 200
    assert r.json() == {
        "username":
        "******",
        "name":
        "GitHub User",
        "email":
        "*****@*****.**",
        "uid":
        123456,
        "groups": [
            {
                "name": "org-a-team",
                "id": 1000
            },
            {
                "name": "org-other-team",
                "id": 1001
            },
            {
                "name": "other-org-team-with-very--F279yg",
                "id": 1002
            },
        ],
    }