Beispiel #1
0
 def __init__(self, config: LDAPConfig) -> None:
     super().__init__(spec=bonsai.LDAPClient)
     self.config = config
     self.groups = [
         TokenGroup(name="foo", id=1222),
         TokenGroup(name="group-1", id=123123),
         TokenGroup(name="group-2", id=123442),
     ]
     self.query: Optional[str] = None
Beispiel #2
0
async def test_invalid_username(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        uid=4137,
        groups=[TokenGroup(name="foo", id=1000)],
    )
    token_service = factory.create_token_service()
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info,
            scopes=["read:all", "admin:token"],
            ip_address="127.0.0.1",
        )
    data = await token_service.get_data(session_token)
    assert data

    # Cannot create any type of token with an invalid name.
    for user in (
            "<bootstrap>",
            "<internal>",
            "in+valid",
            " invalid",
            "invalid ",
            "in/valid",
            "in@valid",
            "-invalid",
            "invalid-",
            "in--valid",
    ):
        user_info.username = user
        with pytest.raises(PermissionDeniedError):
            await token_service.create_session_token(user_info,
                                                     scopes=[],
                                                     ip_address="127.0.0.1")
        data.username = user
        with pytest.raises(PermissionDeniedError):
            await token_service.create_user_token(data,
                                                  user,
                                                  token_name="n",
                                                  scopes=[],
                                                  ip_address="127.0.0.1")
        with pytest.raises(PermissionDeniedError):
            await token_service.get_notebook_token(data,
                                                   ip_address="127.0.0.1")
        with pytest.raises(PermissionDeniedError):
            await token_service.get_internal_token(data,
                                                   service="s",
                                                   scopes=[],
                                                   ip_address="127.0.0.1")
        with pytest.raises(ValidationError):
            AdminTokenRequest(username=user, token_type=TokenType.service)
        request = AdminTokenRequest(username="******",
                                    token_type=TokenType.service)
        request.username = user
        with pytest.raises(PermissionDeniedError):
            await token_service.create_token_from_admin_request(
                request, data, ip_address="127.0.0.1")
Beispiel #3
0
async def create_session_token(
    factory: ComponentFactory,
    *,
    username: Optional[str] = None,
    group_names: Optional[List[str]] = None,
    scopes: Optional[List[str]] = None,
) -> TokenData:
    """Create a session token.

    Parameters
    ----------
    factory : `gafaelfawr.factory.ComponentFactory`
        Factory used to create services to add the token.
    username : `str`, optional
        Override the username of the generated token.
    group_namess : List[`str`], optional
        Group memberships the generated token should have.
    scopes : List[`str`], optional
        Scope for the generated token.

    Returns
    -------
    data : `gafaelfawr.models.token.TokenData`
        The data for the generated token.
    """
    if not username:
        username = "******"
    if group_names:
        groups = [TokenGroup(name=g, id=1000) for g in group_names]
    else:
        groups = []
    user_info = TokenUserInfo(
        username=username,
        name="Some User",
        email="*****@*****.**",
        uid=1000,
        groups=groups,
    )
    if not scopes:
        scopes = ["user:token"]
    token_service = factory.create_token_service()
    async with factory.session.begin():
        token = await token_service.create_session_token(
            user_info, scopes=scopes, ip_address="127.0.0.1"
        )
    data = await token_service.get_data(token)
    assert data
    return data
Beispiel #4
0
    async def create_user_info(self, code: str, state: str) -> TokenUserInfo:
        """Given the code from an authentication, create the user information.

        Parameters
        ----------
        code : `str`
            Code returned by a successful authentication.
        state : `str`
            The same random string used for the redirect URL.

        Returns
        -------
        user_info : `gafaelfawr.models.token.TokenUserInfo`
            The user information corresponding to that authentication.

        Raises
        ------
        httpx.HTTPError
            An HTTP client error occurred trying to talk to the authentication
            provider.
        gafaelfawr.exceptions.GitHubException
            GitHub responded with an error to a request.
        """
        self._logger.info("Getting user information from GitHub")
        github_token = await self._get_access_token(code, state)
        user_info = await self._get_user_info(github_token)

        groups = []
        invalid_groups = {}
        for team in user_info.teams:
            try:
                groups.append(TokenGroup(name=team.group_name, id=team.gid))
            except ValidationError as e:
                invalid_groups[team.group_name] = str(e)
        if invalid_groups:
            self._logger.warning("Ignoring invalid groups",
                                 invalid_groups=invalid_groups)
        return TokenUserInfo(
            username=user_info.username.lower(),
            name=user_info.name,
            email=user_info.email,
            uid=user_info.uid,
            groups=groups,
        )
Beispiel #5
0
async def test_create_delete_modify(setup: SetupTest,
                                    caplog: LogCaptureFixture) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        email="*****@*****.**",
        uid=45613,
        groups=[TokenGroup(name="foo", id=12313)],
    )
    token_service = setup.factory.create_token_service()
    session_token = await token_service.create_session_token(
        user_info,
        scopes=["read:all", "exec:admin", "user:token"],
        ip_address="127.0.0.1",
    )
    csrf = await setup.login(session_token)

    expires = current_datetime() + timedelta(days=100)
    r = await setup.client.post(
        "/auth/api/v1/users/example/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {"token": ANY}
    user_token = Token.from_str(r.json()["token"])
    token_url = r.headers["Location"]
    assert token_url == f"/auth/api/v1/users/example/tokens/{user_token.key}"

    r = await setup.client.get(token_url)
    assert r.status_code == 200
    info = r.json()
    assert info == {
        "token": user_token.key,
        "username": "******",
        "token_name": "some token",
        "token_type": "user",
        "scopes": ["read:all"],
        "created": ANY,
        "expires": int(expires.timestamp()),
    }

    # Check that this is the same information as is returned by the token-info
    # route.  This is a bit tricky to do since the cookie will take precedence
    # over the Authorization header, but we can't just delete the cookie since
    # we'll lose the CSRF token.  Save the cookie and delete it, and then
    # later restore it.
    cookie = setup.client.cookies.pop(COOKIE_NAME)
    r = await setup.client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {user_token}"},
    )
    assert r.status_code == 200
    assert r.json() == info
    setup.client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME)

    # Listing all tokens for this user should return the user token and a
    # session token.
    r = await setup.client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    assert r.json() == sorted(
        [
            {
                "token": session_token.key,
                "username": "******",
                "token_type": "session",
                "scopes": ["exec:admin", "read:all", "user:token"],
                "created": ANY,
                "expires": ANY,
            },
            info,
        ],
        key=lambda t: t["token"],
    )

    # Change the name, scope, and expiration of the token.
    caplog.clear()
    new_expires = current_datetime() + timedelta(days=200)
    r = await setup.client.patch(
        token_url,
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {
        "token": user_token.key,
        "username": "******",
        "token_name": "happy token",
        "token_type": "user",
        "scopes": ["exec:admin"],
        "created": ANY,
        "expires": int(new_expires.timestamp()),
    }

    # Check the logging.  Regression test for a bug where new expirations
    # would be logged as raw datetime objects instead of timestamps.
    log = json.loads(caplog.record_tuples[0][2])
    assert log == {
        "expires": int(new_expires.timestamp()),
        "event": "Modified token",
        "key": user_token.key,
        "level": "info",
        "logger": "gafaelfawr",
        "method": "PATCH",
        "path": token_url,
        "remote": "127.0.0.1",
        "request_id": ANY,
        "scope": "exec:admin read:all user:token",
        "token": session_token.key,
        "token_name": "happy token",
        "token_scope": "exec:admin",
        "token_source": "cookie",
        "user": "******",
        "user_agent": ANY,
    }

    # Delete the token.
    r = await setup.client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 204
    r = await setup.client.get(token_url)
    assert r.status_code == 404

    # Deleting again should return 404.
    r = await setup.client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 404

    # This user should now have only one token.
    r = await setup.client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    assert len(r.json()) == 1

    # We should be able to see the change history for the token.
    r = await setup.client.get(token_url + "/change-history")
    assert r.status_code == 200
    assert r.json() == [
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "revoke",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "edit",
            "old_token_name": "some token",
            "old_scopes": ["read:all"],
            "old_expires": int(expires.timestamp()),
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
            "actor": "example",
            "action": "create",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
    ]
Beispiel #6
0
async def test_token_info(setup: SetupTest) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        email="*****@*****.**",
        uid=45613,
        groups=[TokenGroup(name="foo", id=12313)],
    )
    token_service = setup.factory.create_token_service()
    session_token = await token_service.create_session_token(
        user_info, scopes=["exec:admin", "user:token"], ip_address="127.0.0.1")

    r = await setup.client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {session_token}"},
    )
    assert r.status_code == 200
    data = r.json()
    assert data == {
        "token": session_token.key,
        "username": "******",
        "token_type": "session",
        "scopes": ["exec:admin", "user:token"],
        "created": ANY,
        "expires": ANY,
    }
    now = datetime.now(tz=timezone.utc)
    created = datetime.fromtimestamp(data["created"], tz=timezone.utc)
    assert now - timedelta(seconds=2) <= created <= now
    expires = created + timedelta(minutes=setup.config.issuer.exp_minutes)
    assert datetime.fromtimestamp(data["expires"], tz=timezone.utc) == expires

    r = await setup.client.get(
        "/auth/api/v1/user-info",
        headers={"Authorization": f"bearer {session_token}"},
    )
    assert r.status_code == 200
    session_user_info = r.json()
    assert session_user_info == {
        "username": "******",
        "name": "Example Person",
        "email": "*****@*****.**",
        "uid": 45613,
        "groups": [{
            "name": "foo",
            "id": 12313,
        }],
    }

    # Check the same with a user token, which has some additional associated
    # data.
    expires = now + timedelta(days=100)
    data = await token_service.get_data(session_token)
    user_token = await token_service.create_user_token(
        data,
        data.username,
        token_name="some-token",
        scopes=["exec:admin"],
        expires=expires,
        ip_address="127.0.0.1",
    )

    r = await setup.client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {user_token}"},
    )
    assert r.status_code == 200
    data = r.json()
    assert data == {
        "token": user_token.key,
        "username": "******",
        "token_type": "user",
        "token_name": "some-token",
        "scopes": ["exec:admin"],
        "created": ANY,
        "expires": int(expires.timestamp()),
    }

    r = await setup.client.get(
        "/auth/api/v1/user-info",
        headers={"Authorization": f"bearer {user_token}"},
    )
    assert r.status_code == 200
    assert r.json() == session_user_info

    # Test getting a list of tokens for a user.
    state = State(token=session_token)
    r = await setup.client.get(
        "/auth/api/v1/users/example/tokens",
        cookies={COOKIE_NAME: state.as_cookie()},
    )
Beispiel #7
0
async def test_create_delete_modify(
    client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture
) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        email="*****@*****.**",
        uid=45613,
        groups=[TokenGroup(name="foo", id=12313)],
    )
    token_service = factory.create_token_service()
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info,
            scopes=["read:all", "exec:admin", "user:token"],
            ip_address="127.0.0.1",
        )
    csrf = await set_session_cookie(client, session_token)

    expires = current_datetime() + timedelta(days=100)
    r = await client.post(
        "/auth/api/v1/users/example/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {"token": ANY}
    user_token = Token.from_str(r.json()["token"])
    token_url = r.headers["Location"]
    assert token_url == f"/auth/api/v1/users/example/tokens/{user_token.key}"

    r = await client.get(token_url)
    assert r.status_code == 200
    info = r.json()
    assert info == {
        "token": user_token.key,
        "username": "******",
        "token_name": "some token",
        "token_type": "user",
        "scopes": ["read:all"],
        "created": ANY,
        "expires": int(expires.timestamp()),
    }

    # Check that this is the same information as is returned by the token-info
    # route.  This is a bit tricky to do since the cookie will take precedence
    # over the Authorization header, but we can't just delete the cookie since
    # we'll lose the CSRF token.  Save the cookie and delete it, and then
    # later restore it.
    cookie = client.cookies.pop(COOKIE_NAME)
    r = await client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {user_token}"},
    )
    assert r.status_code == 200
    assert r.json() == info
    client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME)

    # Listing all tokens for this user should return the user token and a
    # session token.
    r = await client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    data = r.json()

    # Adjust for sorting, which will be by creation date and then token.
    assert len(data) == 2
    if data[0] == info:
        session_info = data[1]
    else:
        assert data[1] == info
        session_info = data[0]
    assert session_info == {
        "token": session_token.key,
        "username": "******",
        "token_type": "session",
        "scopes": ["exec:admin", "read:all", "user:token"],
        "created": ANY,
        "expires": ANY,
    }

    # Change the name, scope, and expiration of the token.
    caplog.clear()
    new_expires = current_datetime() + timedelta(days=200)
    r = await client.patch(
        token_url,
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {
        "token": user_token.key,
        "username": "******",
        "token_name": "happy token",
        "token_type": "user",
        "scopes": ["exec:admin"],
        "created": ANY,
        "expires": int(new_expires.timestamp()),
    }

    # Check the logging.  Regression test for a bug where new expirations
    # would be logged as raw datetime objects instead of timestamps.
    assert parse_log(caplog) == [
        {
            "expires": int(new_expires.timestamp()),
            "event": "Modified token",
            "httpRequest": {
                "requestMethod": "PATCH",
                "requestUrl": f"https://{TEST_HOSTNAME}{token_url}",
                "remoteIp": "127.0.0.1",
            },
            "key": user_token.key,
            "scope": "exec:admin read:all user:token",
            "severity": "info",
            "token": session_token.key,
            "token_name": "happy token",
            "token_scope": "exec:admin",
            "token_source": "cookie",
            "user": "******",
        }
    ]

    # Delete the token.
    r = await client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 204
    r = await client.get(token_url)
    assert r.status_code == 404

    # Deleting again should return 404.
    r = await client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 404

    # This user should now have only one token.
    r = await client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    assert len(r.json()) == 1

    # We should be able to see the change history for the token.
    r = await client.get(token_url + "/change-history")
    assert r.status_code == 200
    assert r.json() == [
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "revoke",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "edit",
            "old_token_name": "some token",
            "old_scopes": ["read:all"],
            "old_expires": int(expires.timestamp()),
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
            "actor": "example",
            "action": "create",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
    ]
Beispiel #8
0
async def test_token_from_admin_request(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(username="******",
                              name="Example Person",
                              uid=4137)
    token_service = factory.create_token_service()
    async with factory.session.begin():
        token = await token_service.create_session_token(
            user_info, scopes=[], ip_address="127.0.0.1")
    data = await token_service.get_data(token)
    assert data
    expires = current_datetime() + timedelta(days=2)
    request = AdminTokenRequest(
        username="******",
        token_type=TokenType.user,
        token_name="some token",
        scopes=["read:all"],
        expires=expires,
        name="Other User",
        uid=1345,
        groups=[TokenGroup(name="some-group", id=4133)],
    )

    # Cannot create a token via admin request because the authentication
    # information is missing the admin:token scope.
    with pytest.raises(PermissionDeniedError):
        await token_service.create_token_from_admin_request(
            request, data, ip_address="127.0.0.1")

    # Get a token with an appropriate scope.
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info, scopes=["admin:token"], ip_address="127.0.0.1")
    admin_data = await token_service.get_data(session_token)
    assert admin_data

    # Test a few more errors.
    request.scopes = ["bogus:scope"]
    with pytest.raises(InvalidScopesError):
        await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    request.scopes = ["read:all"]
    request.expires = current_datetime()
    with pytest.raises(InvalidExpiresError):
        await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    request.expires = expires

    # Try a successful request.
    async with factory.session.begin():
        token = await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    user_data = await token_service.get_data(token)
    assert user_data and user_data == TokenData(
        token=token, created=user_data.created, **request.dict())
    assert_is_now(user_data.created)

    async with factory.session.begin():
        history = await token_service.get_change_history(
            admin_data, token=token.key, username=request.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=token.key,
            username=request.username,
            token_type=TokenType.user,
            token_name=request.token_name,
            scopes=["read:all"],
            expires=request.expires,
            actor=admin_data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=user_data.created,
        )
    ]

    # Non-admins can't see other people's tokens.
    with pytest.raises(PermissionDeniedError):
        await token_service.get_change_history(data,
                                               token=token.key,
                                               username=request.username)

    # Now request a service token with minimal data instead.
    request = AdminTokenRequest(username="******",
                                token_type=TokenType.service)
    async with factory.session.begin():
        token = await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    service_data = await token_service.get_data(token)
    assert service_data and service_data == TokenData(
        token=token, created=service_data.created, **request.dict())
    assert_is_now(service_data.created)

    async with factory.session.begin():
        history = await token_service.get_change_history(
            admin_data, token=token.key, username=request.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=token.key,
            username=request.username,
            token_type=TokenType.service,
            scopes=[],
            expires=None,
            actor=admin_data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=service_data.created,
        )
    ]
Beispiel #9
0
async def test_session_token(config: Config,
                             factory: ComponentFactory) -> None:
    token_service = factory.create_token_service()
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        uid=4137,
        groups=[
            TokenGroup(name="group", id=1000),
            TokenGroup(name="another", id=3134),
        ],
    )

    async with factory.session.begin():
        token = await token_service.create_session_token(
            user_info, scopes=["user:token"], ip_address="127.0.0.1")
    data = await token_service.get_data(token)
    assert data and data == TokenData(
        token=token,
        username="******",
        token_type=TokenType.session,
        scopes=["user:token"],
        created=data.created,
        expires=data.expires,
        name="Example Person",
        uid=4137,
        groups=[
            TokenGroup(name="group", id=1000),
            TokenGroup(name="another", id=3134),
        ],
    )
    assert_is_now(data.created)
    expires = data.created + timedelta(minutes=config.issuer.exp_minutes)
    assert data.expires == expires

    async with factory.session.begin():
        info = await token_service.get_token_info_unchecked(token.key)
    assert info and info == TokenInfo(
        token=token.key,
        username=user_info.username,
        token_name=None,
        token_type=TokenType.session,
        scopes=data.scopes,
        created=int(data.created.timestamp()),
        last_used=None,
        expires=int(data.expires.timestamp()),
        parent=None,
    )
    assert await token_service.get_user_info(token) == user_info

    async with factory.session.begin():
        history = await token_service.get_change_history(
            data, token=token.key, username=data.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=token.key,
            username=data.username,
            token_type=TokenType.session,
            scopes=["user:token"],
            expires=data.expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=data.created,
        )
    ]

    # Test a session token with scopes.
    async with factory.session.begin():
        token = await token_service.create_session_token(
            user_info,
            scopes=["read:all", "exec:admin"],
            ip_address="127.0.0.1",
        )
        data = await token_service.get_data(token)
        assert data and data.scopes == ["exec:admin", "read:all"]
        info = await token_service.get_token_info_unchecked(token.key)
        assert info and info.scopes == ["exec:admin", "read:all"]
Beispiel #10
0
async def test_internal_token(config: Config,
                              factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        uid=4137,
        groups=[TokenGroup(name="foo", id=1000)],
    )
    token_service = factory.create_token_service()
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info,
            scopes=["read:all", "exec:admin", "user:token"],
            ip_address="127.0.0.1",
        )
    data = await token_service.get_data(session_token)
    assert data

    async with factory.session.begin():
        internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:all"],
            ip_address="2001:db8::45",
        )
        assert await token_service.get_user_info(internal_token) == user_info
        info = await token_service.get_token_info_unchecked(internal_token.key)
    assert info and info == TokenInfo(
        token=internal_token.key,
        username=user_info.username,
        token_type=TokenType.internal,
        service="some-service",
        scopes=["read:all"],
        created=info.created,
        last_used=None,
        expires=data.expires,
        parent=session_token.key,
    )
    assert_is_now(info.created)

    # Cannot request a scope that the parent token doesn't have.
    with pytest.raises(InvalidScopesError):
        await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:some"],
            ip_address="127.0.0.1",
        )

    # Creating another internal token from the same parent token with the same
    # parameters just returns the same internal token as before.
    new_internal_token = await token_service.get_internal_token(
        data,
        service="some-service",
        scopes=["read:all"],
        ip_address="127.0.0.1",
    )
    assert internal_token == new_internal_token

    # Try again with the cache cleared to force a database lookup.
    await token_service._token_cache.clear()
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:all"],
            ip_address="127.0.0.1",
        )
    assert internal_token == new_internal_token

    async with factory.session.begin():
        history = await token_service.get_change_history(
            data, token=internal_token.key, username=data.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=internal_token.key,
            username=data.username,
            token_type=TokenType.internal,
            parent=data.token.key,
            service="some-service",
            scopes=["read:all"],
            expires=data.expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address="2001:db8::45",
            event_time=info.created,
        )
    ]

    # It's possible we'll have a race condition where two workers both create
    # an internal token at the same time with the same parameters.  Gafaelfawr
    # 3.0.2 had a regression where, once that had happened, it could not
    # retrieve the internal token because it didn't expect multiple results
    # from the query.  Simulate this and make sure it's handled properly.  The
    # easiest way to do this is to use the internals of the token service.
    second_internal_token = Token()
    created = current_datetime()
    expires = created + config.token_lifetime
    internal_token_data = TokenData(
        token=second_internal_token,
        username=data.username,
        token_type=TokenType.internal,
        scopes=["read:all"],
        created=created,
        expires=expires,
        name=data.name,
        email=data.email,
        uid=data.uid,
        groups=data.groups,
    )
    await token_service._token_redis_store.store_data(internal_token_data)
    async with factory.session.begin():
        await token_service._token_db_store.add(internal_token_data,
                                                service="some-service",
                                                parent=data.token.key)
    await token_service._token_cache.clear()
    async with factory.session.begin():
        dup_internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:all"],
            ip_address="127.0.0.1",
        )
    assert dup_internal_token in (internal_token, second_internal_token)

    # A different scope or a different service results in a new token.
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["exec:admin"],
            ip_address="127.0.0.1",
        )
    assert internal_token != new_internal_token
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data,
            service="another-service",
            scopes=["read:all"],
            ip_address="127.0.0.1",
        )
    assert internal_token != new_internal_token

    # Check that the expiration time is capped by creating a user token that
    # doesn't expire and then creating a notebook token from it.  Use this to
    # test a token with empty scopes.
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            data,
            data.username,
            token_name="some token",
            scopes=["exec:admin"],
            expires=None,
            ip_address="127.0.0.1",
        )
    data = await token_service.get_data(user_token)
    assert data
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data, service="some-service", scopes=[], ip_address="127.0.0.1")
        assert new_internal_token != internal_token
        info = await token_service.get_token_info_unchecked(
            new_internal_token.key)
    assert info and info.scopes == []
    expires = info.created + timedelta(minutes=config.issuer.exp_minutes)
    assert info.expires == expires
Beispiel #11
0
async def test_internal_token(setup: SetupTest) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        uid=4137,
        groups=[TokenGroup(name="foo", id=1000)],
    )
    token_service = setup.factory.create_token_service()
    session_token = await token_service.create_session_token(
        user_info,
        scopes=["read:all", "exec:admin", "user:token"],
        ip_address="127.0.0.1",
    )
    data = await token_service.get_data(session_token)
    assert data

    internal_token = await token_service.get_internal_token(
        data,
        service="some-service",
        scopes=["read:all"],
        ip_address="2001:db8::45",
    )
    assert await token_service.get_user_info(internal_token) == user_info
    info = token_service.get_token_info_unchecked(internal_token.key)
    assert info and info == TokenInfo(
        token=internal_token.key,
        username=user_info.username,
        token_type=TokenType.internal,
        service="some-service",
        scopes=["read:all"],
        created=info.created,
        last_used=None,
        expires=data.expires,
        parent=session_token.key,
    )
    assert_is_now(info.created)

    # Cannot request a scope that the parent token doesn't have.
    with pytest.raises(InvalidScopesError):
        await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:some"],
            ip_address="127.0.0.1",
        )

    # Creating another internal token from the same parent token with the same
    # parameters just returns the same internal token as before.
    new_internal_token = await token_service.get_internal_token(
        data,
        service="some-service",
        scopes=["read:all"],
        ip_address="127.0.0.1",
    )
    assert internal_token == new_internal_token

    history = token_service.get_change_history(
        data, token=internal_token.key, username=data.username
    )
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=internal_token.key,
            username=data.username,
            token_type=TokenType.internal,
            parent=data.token.key,
            service="some-service",
            scopes=["read:all"],
            expires=data.expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address="2001:db8::45",
            event_time=info.created,
        )
    ]

    # A different scope or a different service results in a new token.
    new_internal_token = await token_service.get_internal_token(
        data,
        service="some-service",
        scopes=["exec:admin"],
        ip_address="127.0.0.1",
    )
    assert internal_token != new_internal_token
    new_internal_token = await token_service.get_internal_token(
        data,
        service="another-service",
        scopes=["read:all"],
        ip_address="127.0.0.1",
    )
    assert internal_token != new_internal_token

    # Check that the expiration time is capped by creating a user token that
    # doesn't expire and then creating a notebook token from it.  Use this to
    # test a token with empty scopes.
    user_token = await token_service.create_user_token(
        data,
        data.username,
        token_name="some token",
        scopes=["exec:admin"],
        expires=None,
        ip_address="127.0.0.1",
    )
    data = await token_service.get_data(user_token)
    assert data
    new_internal_token = await token_service.get_internal_token(
        data, service="some-service", scopes=[], ip_address="127.0.0.1"
    )
    assert new_internal_token != internal_token
    info = token_service.get_token_info_unchecked(new_internal_token.key)
    assert info and info.scopes == []
    expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes)
    assert info.expires == expires
Beispiel #12
0
async def test_notebook_token(setup: SetupTest) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        uid=4137,
        groups=[TokenGroup(name="foo", id=1000)],
    )
    token_service = setup.factory.create_token_service()
    session_token = await token_service.create_session_token(
        user_info,
        scopes=["read:all", "exec:admin", "user:token"],
        ip_address="127.0.0.1",
    )
    data = await token_service.get_data(session_token)
    assert data

    token = await token_service.get_notebook_token(data, ip_address="1.0.0.1")
    assert await token_service.get_user_info(token) == user_info
    info = token_service.get_token_info_unchecked(token.key)
    assert info and info == TokenInfo(
        token=token.key,
        username=user_info.username,
        token_type=TokenType.notebook,
        scopes=["exec:admin", "read:all", "user:token"],
        created=info.created,
        last_used=None,
        expires=data.expires,
        parent=session_token.key,
    )
    assert_is_now(info.created)
    assert await token_service.get_data(token) == TokenData(
        token=token,
        username=user_info.username,
        token_type=TokenType.notebook,
        scopes=["exec:admin", "read:all", "user:token"],
        created=info.created,
        expires=data.expires,
        name=user_info.name,
        uid=user_info.uid,
        groups=user_info.groups,
    )

    # Creating another notebook token from the same parent token just returns
    # the same notebook token as before.
    new_token = await token_service.get_notebook_token(
        data, ip_address="127.0.0.1"
    )
    assert token == new_token

    history = token_service.get_change_history(
        data, token=token.key, username=data.username
    )
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=token.key,
            username=data.username,
            token_type=TokenType.notebook,
            parent=data.token.key,
            scopes=["exec:admin", "read:all", "user:token"],
            expires=data.expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address="1.0.0.1",
            event_time=info.created,
        )
    ]

    # Check that the expiration time is capped by creating a user token that
    # doesn't expire and then creating a notebook token from it.
    user_token = await token_service.create_user_token(
        data,
        data.username,
        token_name="some token",
        scopes=[],
        expires=None,
        ip_address="127.0.0.1",
    )
    data = await token_service.get_data(user_token)
    assert data
    new_token = await token_service.get_notebook_token(
        data, ip_address="127.0.0.1"
    )
    assert new_token != token
    info = token_service.get_token_info_unchecked(new_token.key)
    assert info
    expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes)
    assert info.expires == expires
Beispiel #13
0
    async def create_user_info(self, code: str, state: str) -> TokenUserInfo:
        """Given the code from a successful authentication, get a token.

        Parameters
        ----------
        code : `str`
            Code returned by a successful authentication.
        state : `str`
            The same random string used for the redirect URL.

        Returns
        -------
        user_info : `gafaelfawr.models.token.TokenUserInfo`
            The user information corresponding to that authentication.

        Raises
        ------
        gafaelfawr.exceptions.OIDCException
            The OpenID Connect provider responded with an error to a request.
        httpx.HTTPError
            An HTTP client error occurred trying to talk to the authentication
            provider.
        jwt.exceptions.InvalidTokenError
            The token returned by the OpenID Connect provider was invalid.
        """
        data = {
            "grant_type": "authorization_code",
            "client_id": self._config.client_id,
            "client_secret": self._config.client_secret,
            "code": code,
            "redirect_uri": self._config.redirect_url,
        }
        self._logger.info("Retrieving ID token from %s",
                          self._config.token_url)
        r = await self._http_client.post(
            self._config.token_url,
            data=data,
            headers={"Accept": "application/json"},
        )

        # If the call failed, try to extract an error from the reply.  If that
        # fails, just raise an exception for the HTTP status.
        try:
            result = r.json()
        except Exception:
            if r.status_code != 200:
                r.raise_for_status()
            else:
                msg = "Response from {self._config.token_url} not valid JSON"
                raise OIDCException(msg)
        if r.status_code != 200 and "error" in result:
            msg = result["error"] + ": " + result["error_description"]
            raise OIDCException(msg)
        elif r.status_code != 200:
            r.raise_for_status()
        if "id_token" not in result:
            msg = f"No id_token in token reply from {self._config.token_url}"
            raise OIDCException(msg)

        # Extract and verify the token.
        unverified_token = OIDCToken(encoded=result["id_token"])
        try:
            token = await self._verifier.verify_oidc_token(unverified_token)
        except (jwt.InvalidTokenError, VerifyTokenException) as e:
            msg = f"OpenID Connect token verification failed: {str(e)}"
            raise OIDCException(msg)

        # Extract information from it to create the user information.
        groups = []
        invalid_groups = {}
        try:
            for oidc_group in token.claims.get("isMemberOf", []):
                if "name" not in oidc_group:
                    continue
                name = oidc_group["name"]
                if "id" not in oidc_group:
                    invalid_groups[name] = "missing id"
                    continue
                gid = int(oidc_group["id"])
                try:
                    groups.append(TokenGroup(name=name, id=gid))
                except ValidationError as e:
                    invalid_groups[name] = str(e)
        except Exception as e:
            msg = f"isMemberOf claim is invalid: {str(e)}"
            raise OIDCException(msg)
        return TokenUserInfo(
            username=token.username,
            name=token.claims.get("name"),
            email=token.claims.get("email"),
            uid=token.uid,
            groups=groups,
        )