コード例 #1
0
async def test_no_scope(
    client: AsyncClient, factory: ComponentFactory
) -> None:
    token_data = await create_session_token(factory)
    username = token_data.username
    token_service = factory.create_token_service()
    async with factory.session.begin():
        token = await token_service.create_user_token(
            token_data,
            token_data.username,
            token_name="user",
            scopes=[],
            ip_address="127.0.0.1",
        )

    r = await client.get(
        f"/auth/api/v1/users/{username}/token-change-history",
        headers={"Authorization": f"bearer {token}"},
    )
    assert r.status_code == 403

    r = await client.get(
        f"/auth/api/v1/users/{username}/tokens/{token.key}/change-history",
        headers={"Authorization": f"bearer {token}"},
    )
    assert r.status_code == 403
コード例 #2
0
async def assert_kubernetes_secrets_are_correct(
    factory: ComponentFactory, mock: MockKubernetesApi, is_fresh: bool = True
) -> None:
    token_service = factory.create_token_service()

    # Get all of the GafaelfawrServiceToken custom objects.
    service_tokens = mock.get_all_objects_for_test("GafaelfawrServiceToken")

    # Calculate the expected secrets.
    expected = [
        V1Secret(
            api_version="v1",
            kind="Secret",
            data={"token": ANY},
            metadata=V1ObjectMeta(
                name=t["metadata"]["name"],
                namespace=t["metadata"]["namespace"],
                annotations=t["metadata"].get("annotations", {}),
                labels=t["metadata"].get("labels", {}),
                owner_references=[
                    V1OwnerReference(
                        api_version="gafaelfawr.lsst.io/v1alpha1",
                        block_owner_deletion=True,
                        controller=True,
                        kind="GafaelfawrServiceToken",
                        name=t["metadata"]["name"],
                        uid=t["metadata"]["uid"],
                    ),
                ],
            ),
            type="Opaque",
        )
        for t in service_tokens
    ]
    expected = sorted(
        expected, key=lambda o: (o.metadata.namespace, o.metadata.name)
    )
    assert mock.get_all_objects_for_test("Secret") == expected

    # Now check that every token in those secrets is correct.
    for service_token in service_tokens:
        name = service_token["metadata"]["name"]
        namespace = service_token["metadata"]["namespace"]
        secret = await mock.read_namespaced_secret(name, namespace)
        data = await token_data_from_secret(token_service, secret)
        assert data == TokenData(
            token=data.token,
            username=service_token["spec"]["service"],
            token_type=TokenType.service,
            scopes=service_token["spec"]["scopes"],
            created=data.created,
            expires=None,
            name=None,
            uid=None,
            groups=None,
        )
        if is_fresh:
            now = current_datetime()
            assert now - timedelta(seconds=5) <= data.created <= now
コード例 #3
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
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")
コード例 #4
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_invalid(config: Config, factory: ComponentFactory) -> None:
    redis = await redis_dependency()
    token_service = factory.create_token_service()
    expires = int(timedelta(days=1).total_seconds())

    # No such key.
    token = Token()
    assert await token_service.get_data(token) is None

    # Invalid encrypted blob.
    await redis.set(f"token:{token.key}", "foo", ex=expires)
    assert await token_service.get_data(token) is None

    # Malformed session.
    fernet = Fernet(config.session_secret.encode())
    raw_data = fernet.encrypt(b"malformed json")
    await redis.set(f"token:{token.key}", raw_data, ex=expires)
    assert await token_service.get_data(token) is None

    # Mismatched token.
    data = TokenData(
        token=Token(),
        username="******",
        token_type=TokenType.session,
        scopes=[],
        created=int(current_datetime().timestamp()),
        name="Some User",
        uid=12345,
    )
    session = fernet.encrypt(data.json().encode())
    await redis.set(f"token:{token.key}", session, ex=expires)
    assert await token_service.get_data(token) is None

    # Missing required fields.
    json_data = {
        "token": {
            "key": token.key,
            "secret": token.secret,
        },
        "token_type": "session",
        "scopes": [],
        "created": int(current_datetime().timestamp()),
        "name": "Some User",
    }
    raw_data = fernet.encrypt(json.dumps(json_data).encode())
    await redis.set(f"token:{token.key}", raw_data, ex=expires)
    assert await token_service.get_data(token) is None

    # Fix the session store and confirm we can retrieve the manually-stored
    # session.
    json_data["username"] = "******"
    raw_data = fernet.encrypt(json.dumps(json_data).encode())
    await redis.set(f"token:{token.key}", raw_data, ex=expires)
    new_data = await token_service.get_data(token)
    assert new_data == TokenData.parse_obj(json_data)
コード例 #5
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_list(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(username="******",
                              name="Example Person",
                              uid=4137)
    token_service = factory.create_token_service()
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info, scopes=["user:token"], ip_address="127.0.0.1")
    data = await token_service.get_data(session_token)
    assert data
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            data,
            data.username,
            token_name="some-token",
            scopes=[],
            ip_address="127.0.0.1",
        )
        other_user_info = TokenUserInfo(username="******",
                                        name="Other Person",
                                        uid=1313)
        other_session_token = await token_service.create_session_token(
            other_user_info, scopes=["admin:token"], ip_address="1.1.1.1")
    admin_data = await token_service.get_data(other_session_token)
    assert admin_data

    async with factory.session.begin():
        session_info = await token_service.get_token_info_unchecked(
            session_token.key)
        assert session_info
        user_token_info = await token_service.get_token_info_unchecked(
            user_token.key)
        assert user_token_info
        other_session_info = await token_service.get_token_info_unchecked(
            other_session_token.key)
        assert other_session_info
        assert await token_service.list_tokens(data, "example") == sorted(
            sorted((session_info, user_token_info), key=lambda t: t.token),
            key=lambda t: t.created,
            reverse=True,
        )
        assert await token_service.list_tokens(admin_data) == sorted(
            sorted(
                (session_info, other_session_info, user_token_info),
                key=lambda t: t.token,
            ),
            key=lambda t: t.created,
            reverse=True,
        )

    # Regular users can't retrieve all tokens.
    with pytest.raises(PermissionDeniedError):
        await token_service.list_tokens(data)
コード例 #6
0
async def test_no_expires(
    client: AsyncClient, factory: ComponentFactory
) -> None:
    """Test creating a user token that doesn't expire."""
    token_data = await create_session_token(factory)
    csrf = await set_session_cookie(client, token_data.token)

    r = await client.post(
        f"/auth/api/v1/users/{token_data.username}/tokens",
        headers={"X-CSRF-Token": csrf},
        json={"token_name": "some token"},
    )
    assert r.status_code == 201
    token_url = r.headers["Location"]

    r = await client.get(token_url)
    assert "expires" not in r.json()

    # Create a user token with an expiration and then adjust it to not expire.
    now = datetime.now(tz=timezone.utc).replace(microsecond=0)
    expires = now + timedelta(days=2)
    r = await client.post(
        f"/auth/api/v1/users/{token_data.username}/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "another token",
            "expires": int(expires.timestamp()),
        },
    )
    assert r.status_code == 201
    user_token = Token.from_str(r.json()["token"])
    token_service = factory.create_token_service()
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data and user_token_data.expires == expires
    token_url = r.headers["Location"]

    r = await client.get(token_url)
    assert r.json()["expires"] == int(expires.timestamp())

    r = await client.patch(
        token_url,
        headers={"X-CSRF-Token": csrf},
        json={"expires": None},
    )
    assert r.status_code == 201
    assert "expires" not in r.json()

    # Check that the expiration was also changed in Redis.
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data and user_token_data.expires is None
コード例 #7
0
ファイル: tokens.py プロジェクト: lsst-sqre/gafaelfawr
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
コード例 #8
0
async def test_basic(factory: ComponentFactory) -> None:
    token_data = await create_session_token(factory, scopes=["read:all"])
    token_service = factory.create_token_service()
    token_cache = factory.create_token_cache_service()
    async with factory.session.begin():
        internal_token = await token_service.get_internal_token(
            token_data, "some-service", ["read:all"], ip_address="127.0.0.1")
        notebook_token = await token_service.get_notebook_token(
            token_data, ip_address="127.0.0.1")

    assert internal_token == await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"], "127.0.0.1")
    assert notebook_token == await token_cache.get_notebook_token(
        token_data, "127.0.0.1")

    # Requesting different internal tokens doesn't work.
    async with factory.session.begin():
        assert internal_token != await token_cache.get_internal_token(
            token_data, "other-service", ["read:all"], "127.0.0.1")
        assert notebook_token != await token_cache.get_internal_token(
            token_data, "some-service", [], "127.0.0.1")

    # A different service token for the same user requesting the same
    # information creates a different internal token.
    new_token_data = await create_session_token(factory, scopes=["read:all"])
    async with factory.session.begin():
        assert internal_token != await token_cache.get_internal_token(
            new_token_data, "some-service", ["read:all"], "127.0.0.1")
        assert notebook_token != await token_cache.get_notebook_token(
            new_token_data, "127.0.0.1")

    # Changing the scope of the parent token doesn't matter as long as the
    # internal token is requested with the same scope.  Cases where the parent
    # token no longer has that scope are caught one level up by the token
    # service and thus aren't tested here.
    token_data.scopes = ["read:all", "admin:token"]
    assert internal_token == await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"], "127.0.0.1")
    async with factory.session.begin():
        assert internal_token != await token_cache.get_internal_token(
            token_data, "some-service", ["admin:token"], "127.0.0.1")
コード例 #9
0
async def test_token_info(
    client: AsyncClient, config: Config, factory: ComponentFactory
) -> 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=["exec:admin", "user:token"],
            ip_address="127.0.0.1",
        )

    r = await 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=config.issuer.exp_minutes)
    assert datetime.fromtimestamp(data["expires"], tz=timezone.utc) == expires

    r = await 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)
    async with factory.session.begin():
        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 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 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 client.get(
        "/auth/api/v1/users/example/tokens",
        cookies={COOKIE_NAME: await state.as_cookie()},
    )
コード例 #10
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_delete(factory: ComponentFactory) -> None:
    data = await create_session_token(factory)
    token_service = factory.create_token_service()
    async with factory.session.begin():
        token = await token_service.create_user_token(
            data,
            data.username,
            token_name="some token",
            scopes=[],
            ip_address="127.0.0.1",
        )

    async with factory.session.begin():
        assert await token_service.delete_token(token.key,
                                                data,
                                                data.username,
                                                ip_address="127.0.0.1")

    assert await token_service.get_data(token) is None
    async with factory.session.begin():
        assert await token_service.get_token_info_unchecked(token.key) is None
        assert await token_service.get_user_info(token) is None

    async with factory.session.begin():
        assert not await token_service.delete_token(
            token.key, data, data.username, ip_address="127.0.0.1")

    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.user,
            token_name="some token",
            scopes=[],
            expires=None,
            actor=data.username,
            action=TokenChange.revoke,
            ip_address="127.0.0.1",
            event_time=history.entries[0].event_time,
        ),
        TokenChangeHistoryEntry(
            token=token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="some token",
            scopes=[],
            expires=None,
            actor=data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=history.entries[1].event_time,
        ),
    ]

    # Cannot delete someone else's token.
    async with factory.session.begin():
        token = await token_service.create_user_token(
            data,
            data.username,
            token_name="some token",
            scopes=[],
            ip_address="127.0.0.1",
        )
    other_data = await create_session_token(factory, username="******")
    async with factory.session.begin():
        with pytest.raises(PermissionDeniedError):
            await token_service.delete_token(token.key,
                                             other_data,
                                             data.username,
                                             ip_address="127.0.0.1")

    # Admins can delete soemone else's token.
    admin_data = await create_session_token(factory,
                                            username="******",
                                            scopes=["admin:token"])
    assert await token_service.get_data(token)
    async with factory.session.begin():
        assert await token_service.delete_token(token.key,
                                                admin_data,
                                                data.username,
                                                ip_address="127.0.0.1")
    assert await token_service.get_data(token) is None
コード例 #11
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_delete_cascade(factory: ComponentFactory) -> None:
    """Test that deleting a token cascades to child tokens."""
    token_service = factory.create_token_service()
    session_token_data = await create_session_token(
        factory, scopes=["admin:token", "read:all", "user:token"])
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            session_token_data,
            session_token_data.username,
            token_name="user-token",
            scopes=["user:token"],
            ip_address="127.0.0.1",
        )
        user_token_data = await token_service.get_data(user_token)
        assert user_token_data
        admin_request = AdminTokenRequest(
            username="******",
            token_type=TokenType.service,
            scopes=["read:all", "user:token"],
            name="Some Service",
        )
        service_token = await token_service.create_token_from_admin_request(
            admin_request, session_token_data, ip_address="127.0.0.1")
        service_token_data = await token_service.get_data(service_token)
        assert service_token_data

    # Build a tree of tokens hung off of the session token.
    async with factory.session.begin():
        notebook_token = await token_service.get_notebook_token(
            session_token_data, ip_address="127.0.0.1")
        notebook_token_data = await token_service.get_data(notebook_token)
        assert notebook_token_data
        session_children = [
            notebook_token,
            await token_service.get_internal_token(
                session_token_data,
                "service-a",
                scopes=[],
                ip_address="127.0.0.1",
            ),
            await token_service.get_internal_token(
                notebook_token_data,
                "service-b",
                scopes=[],
                ip_address="127.0.0.1",
            ),
            await token_service.get_internal_token(
                notebook_token_data,
                "service-a",
                scopes=["read:all"],
                ip_address="127.0.0.1",
            ),
        ]
        internal_token_data = await token_service.get_data(session_children[-1]
                                                           )
        assert internal_token_data
        session_children.append(await token_service.get_internal_token(
            internal_token_data,
            "service-b",
            scopes=["read:all"],
            ip_address="127.0.0.1",
        ))

    # Shorter trees of tokens from the user and service tokens.
    async with factory.session.begin():
        user_children = [
            await token_service.get_internal_token(user_token_data,
                                                   "service-c",
                                                   scopes=[],
                                                   ip_address="127.0.0.1"),
            await token_service.get_notebook_token(user_token_data,
                                                   ip_address="127.0.0.1"),
        ]
        service_children = [
            await token_service.get_internal_token(
                service_token_data,
                "service-a",
                scopes=[],
                ip_address="127.0.0.1",
            )
        ]

    # Deleting the session token should invalidate all of its children.
    async with factory.session.begin():
        assert await token_service.delete_token(
            session_token_data.token.key,
            session_token_data,
            session_token_data.username,
            ip_address="127.0.0.1",
        )
    for token in session_children:
        assert await token_service.get_data(token) is None

    # But the user and service token created by this token should not be
    # deleted.
    assert await token_service.get_data(user_token_data.token)
    assert await token_service.get_data(service_token_data.token)

    # Deleting those tokens should cascade to their children.
    async with factory.session.begin():
        assert await token_service.delete_token(
            user_token_data.token.key,
            user_token_data,
            user_token_data.username,
            ip_address="127.0.0.1",
        )
    for token in user_children:
        assert await token_service.get_data(token) is None
    async with factory.session.begin():
        assert await token_service.delete_token(
            service_token_data.token.key,
            service_token_data,
            service_token_data.username,
            ip_address="127.0.0.1",
        )
    for token in service_children:
        assert await token_service.get_data(token) is None
コード例 #12
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_modify_expires(config: Config,
                              factory: ComponentFactory) -> None:
    """Test that expiration changes cascade to subtokens."""
    token_service = factory.create_token_service()
    session_token_data = await create_session_token(factory,
                                                    scopes=["user:token"])

    # Create a user token with no expiration and some additional tokens
    # chained off of it.
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            session_token_data,
            session_token_data.username,
            token_name="user-token",
            scopes=["user:token"],
            ip_address="127.0.0.1",
        )
        user_token_data = await token_service.get_data(user_token)
        assert user_token_data
        notebook_token = await token_service.get_notebook_token(
            user_token_data, ip_address="127.0.0.1")
        notebook_token_data = await token_service.get_data(notebook_token)
        assert notebook_token_data
        internal_token = await token_service.get_internal_token(
            user_token_data, "service-a", scopes=[], ip_address="127.0.0.1")
        internal_token_data = await token_service.get_data(internal_token)
        assert internal_token_data
        nested_token = await token_service.get_internal_token(
            notebook_token_data,
            "service-b",
            scopes=[],
            ip_address="127.0.0.1")
        nested_token_data = await token_service.get_data(nested_token)
        assert nested_token_data

    # Check the expiration of all of those tokens matches the default
    # expiration for generated tokens.
    delta = timedelta(minutes=config.issuer.exp_minutes)
    assert notebook_token_data.expires == notebook_token_data.created + delta
    assert internal_token_data.expires == internal_token_data.created + delta
    assert nested_token_data.expires == notebook_token_data.expires

    # Check that Redis also has an appropriate TTL.
    redis = await redis_dependency()
    ttl = delta.total_seconds()
    for token in (notebook_token, internal_token, nested_token):
        assert ttl - 5 <= await redis.ttl(f"token:{token.key}") <= ttl

    # Change the expiration of the user token.
    new_delta = timedelta(minutes=config.issuer.exp_minutes / 2)
    new_expires = user_token_data.created + new_delta
    async with factory.session.begin():
        await token_service.modify_token(
            user_token.key,
            user_token_data,
            expires=new_expires,
            ip_address="127.0.0.1",
        )

    # Check that all of the tokens have been updated.
    notebook_token_data = await token_service.get_data(notebook_token)
    assert notebook_token_data
    internal_token_data = await token_service.get_data(internal_token)
    assert internal_token_data
    nested_token_data = await token_service.get_data(nested_token)
    assert nested_token_data
    assert notebook_token_data.expires == new_expires
    assert internal_token_data.expires == new_expires
    assert nested_token_data.expires == new_expires

    # Check that the Redis TTL has also been updated.
    ttl = new_delta.total_seconds()
    for token in (notebook_token, internal_token, nested_token):
        assert ttl - 5 <= await redis.ttl(f"token:{token.key}") <= ttl
コード例 #13
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_modify(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(username="******",
                              name="Example Person",
                              uid=4137)
    token_service = factory.create_token_service()
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info,
            scopes=["read:all", "user:token"],
            ip_address="127.0.0.1",
        )
    data = await token_service.get_data(session_token)
    assert data
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            data,
            data.username,
            token_name="some-token",
            scopes=[],
            ip_address="127.0.0.1",
        )

    expires = current_datetime() + timedelta(days=50)
    async with factory.session.begin():
        await token_service.modify_token(
            user_token.key,
            data,
            token_name="happy token",
            ip_address="127.0.0.1",
        )
        await token_service.modify_token(
            user_token.key,
            data,
            scopes=["read:all"],
            expires=expires,
            ip_address="192.168.0.4",
        )
        info = await token_service.get_token_info_unchecked(user_token.key)
    assert info and info == TokenInfo(
        token=user_token.key,
        username="******",
        token_type=TokenType.user,
        token_name="happy token",
        scopes=["read:all"],
        created=info.created,
        expires=expires,
        last_used=None,
        parent=None,
    )
    async with factory.session.begin():
        await token_service.modify_token(
            user_token.key,
            data,
            expires=None,
            no_expire=True,
            ip_address="127.0.4.5",
        )

    async with factory.session.begin():
        history = await token_service.get_change_history(
            data, token=user_token.key, username=data.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="happy token",
            scopes=["read:all"],
            expires=None,
            actor=data.username,
            action=TokenChange.edit,
            old_expires=expires,
            ip_address="127.0.4.5",
            event_time=history.entries[0].event_time,
        ),
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="happy token",
            scopes=["read:all"],
            expires=expires,
            actor=data.username,
            action=TokenChange.edit,
            old_scopes=[],
            old_expires=None,
            ip_address="192.168.0.4",
            event_time=history.entries[1].event_time,
        ),
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="happy token",
            scopes=[],
            expires=None,
            actor=data.username,
            action=TokenChange.edit,
            old_token_name="some-token",
            ip_address="127.0.0.1",
            event_time=history.entries[2].event_time,
        ),
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="some-token",
            scopes=[],
            expires=None,
            actor=data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=info.created,
        ),
    ]
コード例 #14
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_child_token_lifetime(config: Config,
                                    factory: ComponentFactory) -> None:
    """Test that a new internal token is generated at half its lifetime."""
    session_token_data = await create_session_token(factory)
    token_service = factory.create_token_service()

    # Generate a user token with a lifetime less than half of the default
    # lifetime for an internal token.  This will get us a short-lived internal
    # token that should be ineligible for handing out for a user token that
    # doesn't expire.
    delta = timedelta(minutes=(config.issuer.exp_minutes / 2) - 5)
    expires = current_datetime() + delta
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            session_token_data,
            session_token_data.username,
            token_name="n",
            expires=expires,
            scopes=[],
            ip_address="127.0.0.1",
        )
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data

    # Get an internal token and ensure we get the same one when we ask again.
    async with factory.session.begin():
        internal_token = await token_service.get_internal_token(
            user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    internal_token_data = await token_service.get_data(internal_token)
    assert internal_token_data
    assert internal_token_data.expires == user_token_data.expires
    new_internal_token = await token_service.get_internal_token(
        user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    assert new_internal_token == internal_token

    # Do the same thing with a notebook token.
    async with factory.session.begin():
        notebook_token = await token_service.get_notebook_token(
            user_token_data, ip_address="127.0.0.1")
    notebook_token_data = await token_service.get_data(notebook_token)
    assert notebook_token_data
    assert notebook_token_data.expires == user_token_data.expires
    new_notebook_token = await token_service.get_notebook_token(
        user_token_data, ip_address="127.0.0.1")
    assert new_notebook_token == notebook_token

    # Change the expiration of the user token to longer than the maximum
    # internal token lifetime.
    new_delta = timedelta(minutes=config.issuer.exp_minutes * 2)
    expires = current_datetime() + new_delta
    async with factory.session.begin():
        assert await token_service.modify_token(
            user_token.key,
            session_token_data,
            session_token_data.username,
            ip_address="127.0.0.1",
            expires=expires,
        )
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data

    # Now, request an internal and notebook token.  We should get different
    # ones with a longer expiration.
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    assert new_internal_token != internal_token
    internal_token = new_internal_token
    internal_token_data = await token_service.get_data(internal_token)
    assert internal_token_data
    delta = timedelta(minutes=config.issuer.exp_minutes)
    assert internal_token_data.expires == internal_token_data.created + delta
    async with factory.session.begin():
        new_notebook_token = await token_service.get_notebook_token(
            user_token_data, ip_address="127.0.0.1")
    assert new_notebook_token != notebook_token
    notebook_token = new_notebook_token
    notebook_token_data = await token_service.get_data(notebook_token)
    assert notebook_token_data
    assert notebook_token_data.expires == notebook_token_data.created + delta

    # Change the expiration of the user token to no longer expire.
    async with factory.session.begin():
        assert await token_service.modify_token(
            user_token.key,
            session_token_data,
            session_token_data.username,
            ip_address="127.0.0.1",
            expires=None,
            no_expire=True,
        )
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data

    # Get an internal and notebook token again.  We should get the same ones
    # as last time.
    new_internal_token = await token_service.get_internal_token(
        user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    assert new_internal_token == internal_token
    new_notebook_token = await token_service.get_notebook_token(
        user_token_data, ip_address="127.0.0.1")
    assert new_notebook_token == notebook_token
コード例 #15
0
async def test_wrong_user(
    client: AsyncClient, factory: ComponentFactory
) -> None:
    token_data = await create_session_token(factory)
    csrf = await set_session_cookie(client, token_data.token)
    token_service = factory.create_token_service()
    user_info = TokenUserInfo(
        username="******", name="Some Other Person", uid=137123
    )
    async with factory.session.begin():
        other_session_token = await token_service.create_session_token(
            user_info, scopes=["user:token"], ip_address="127.0.0.1"
        )
    other_session_data = await token_service.get_data(other_session_token)
    assert other_session_data
    async with factory.session.begin():
        other_token = await token_service.create_user_token(
            other_session_data,
            "other-person",
            token_name="foo",
            scopes=[],
            ip_address="127.0.0.1",
        )

    # Get a token list.
    r = await client.get("/auth/api/v1/users/other-person/tokens")
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Create a new user token.
    r = await client.post(
        "/auth/api/v1/users/other-person/tokens",
        headers={"X-CSRF-Token": csrf},
        json={"token_name": "happy token"},
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Get an individual token.
    r = await client.get(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}"
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Get the history of an individual token.
    r = await client.get(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}"
        "/change-history"
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Ensure you can't see someone else's token under your username either.
    r = await client.get(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}"
    )
    assert r.status_code == 404

    # Or their history.
    r = await client.get(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}"
        "/change-history"
    )
    assert r.status_code == 404

    # Delete a token.
    r = await client.delete(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}",
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"
    r = await client.delete(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}",
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 404

    # Modify a token.
    r = await client.patch(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}",
        json={"token_name": "happy token"},
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"
    r = await client.patch(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}",
        json={"token_name": "happy token"},
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 404
コード例 #16
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
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
コード例 #17
0
async def test_token_errors(
    tmp_path: Path,
    client: AsyncClient,
    factory: ComponentFactory,
    caplog: LogCaptureFixture,
) -> None:
    clients = [
        OIDCClient(client_id="some-id", client_secret="some-secret"),
        OIDCClient(client_id="other-id", client_secret="other-secret"),
    ]
    config = await configure(tmp_path, "github", oidc_clients=clients)
    factory.reconfigure(config)
    token_data = await create_session_token(factory)
    token = token_data.token
    oidc_service = factory.create_oidc_service()
    redirect_uri = f"https://{TEST_HOSTNAME}/app"
    code = await oidc_service.issue_code("some-id", redirect_uri, token)

    # Missing parameters.
    request: Dict[str, str] = {}
    caplog.clear()
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_request",
        "error_description": "Invalid token request",
    }

    assert parse_log(caplog) == [{
        "error": "Invalid token request",
        "event": "Invalid request",
        "httpRequest": {
            "requestMethod": "POST",
            "requestUrl": f"https://{TEST_HOSTNAME}/auth/openid/token",
            "remoteIp": "127.0.0.1",
        },
        "severity": "warning",
    }]

    # Invalid grant type.
    request = {
        "grant_type": "bogus",
        "client_id": "other-client",
        "code": "nonsense",
        "redirect_uri": f"https://{TEST_HOSTNAME}/",
    }
    caplog.clear()
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "unsupported_grant_type",
        "error_description": "Invalid grant type bogus",
    }

    assert parse_log(caplog) == [{
        "error": "Invalid grant type bogus",
        "event": "Unsupported grant type",
        "httpRequest": {
            "requestMethod": "POST",
            "requestUrl": f"https://{TEST_HOSTNAME}/auth/openid/token",
            "remoteIp": "127.0.0.1",
        },
        "severity": "warning",
    }]

    # Invalid code.
    request["grant_type"] = "authorization_code"
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_grant",
        "error_description": "Invalid authorization code",
    }

    # No client_secret.
    request["code"] = str(OIDCAuthorizationCode())
    caplog.clear()
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_client",
        "error_description": "No client_secret provided",
    }

    assert parse_log(caplog) == [{
        "error": "No client_secret provided",
        "event": "Unauthorized client",
        "httpRequest": {
            "requestMethod": "POST",
            "requestUrl": f"https://{TEST_HOSTNAME}/auth/openid/token",
            "remoteIp": "127.0.0.1",
        },
        "severity": "warning",
    }]

    # Incorrect client_id.
    request["client_secret"] = "other-secret"
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_client",
        "error_description": "Unknown client ID other-client",
    }

    # Incorrect client_secret.
    request["client_id"] = "some-id"
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_client",
        "error_description": "Invalid secret for some-id",
    }

    # No stored data.
    request["client_secret"] = "some-secret"
    bogus_code = OIDCAuthorizationCode()
    request["code"] = str(bogus_code)
    caplog.clear()
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_grant",
        "error_description": "Invalid authorization code",
    }
    log = json.loads(caplog.record_tuples[0][2])
    assert log["event"] == "Invalid authorization code"
    assert log["error"] == f"Unknown authorization code {bogus_code.key}"

    # Corrupt stored data.
    redis = await redis_dependency()
    await redis.set(bogus_code.key, "XXXXXXX")
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_grant",
        "error_description": "Invalid authorization code",
    }

    # Correct code, but invalid client_id for that code.
    bogus_code = await oidc_service.issue_code("other-id", redirect_uri, token)
    request["code"] = str(bogus_code)
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_grant",
        "error_description": "Invalid authorization code",
    }

    # Correct code and client_id but invalid redirect_uri.
    request["code"] = str(code)
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_grant",
        "error_description": "Invalid authorization code",
    }

    # Delete the underlying token.
    token_service = factory.create_token_service()
    async with factory.session.begin():
        await token_service.delete_token(token.key,
                                         token_data,
                                         token_data.username,
                                         ip_address="127.0.0.1")
    request["redirect_uri"] = redirect_uri
    r = await client.post("/auth/openid/token", data=request)
    assert r.status_code == 400
    assert r.json() == {
        "error": "invalid_grant",
        "error_description": "Invalid authorization code",
    }
コード例 #18
0
async def test_errors_replace_read(
    factory: ComponentFactory, mock_kubernetes: MockKubernetesApi
) -> None:
    await create_test_service_tokens(mock_kubernetes)
    kubernetes_service = factory.create_kubernetes_service(MagicMock())
    token_service = factory.create_token_service()

    # Create a secret that should exist but has an invalid token.
    secret = V1Secret(
        api_version="v1",
        data={"token": token_as_base64(Token())},
        metadata=V1ObjectMeta(name="gafaelfawr-secret", namespace="mobu"),
        type="Opaque",
    )
    await mock_kubernetes.create_namespaced_secret("mobu", secret)

    # Simulate some errors.  The callback function takes the operation and the
    # secret name.
    def error_callback_replace(method: str, *args: Any) -> None:
        if method in ("replace_namespaced_secret"):
            raise ApiException(status=500, reason="Some error")

    mock_kubernetes.error_callback = error_callback_replace

    # Now run the synchronization.  The secret should be left unchanged, but
    # we should still create the missing nublado2 secret.
    await kubernetes_service.update_service_tokens()
    objects = mock_kubernetes.get_all_objects_for_test("Secret")
    assert secret in objects
    good_secret = await mock_kubernetes.read_namespaced_secret(
        "gafaelfawr", "nublado2"
    )
    assert await token_data_from_secret(token_service, good_secret)

    # We should have also updated the status of the parent custom object.
    service_token = await mock_kubernetes.get_namespaced_custom_object(
        "gafaelfawr.lsst.io",
        "v1alpha1",
        "mobu",
        "gafaelfawrservicetokens",
        "gafaelfawr-secret",
    )
    assert service_token["status"]["conditions"] == [
        {
            "lastTransitionTime": ANY,
            "message": "Kubernetes API error: (500)\nReason: Some error\n",
            "observedGeneration": 1,
            "reason": StatusReason.Failed.value,
            "status": "False",
            "type": "SecretCreated",
        }
    ]

    # Try again, but simulating an error in retrieving a secret.
    def error_callback_read(method: str, *args: Any) -> None:
        if method == "read_namespaced_secret":
            raise ApiException(status=500, reason="Some error")

    mock_kubernetes.error_callback = error_callback_read

    # Now run the synchronization.  As before, the secret should be left
    # unchanged, and the good secret should also be left unchanged.
    await kubernetes_service.update_service_tokens()
    objects = mock_kubernetes.get_all_objects_for_test("Secret")
    assert secret in objects
コード例 #19
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_user_token(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(username="******",
                              name="Example Person",
                              uid=4137)
    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
    expires = current_datetime() + timedelta(days=2)

    # Scopes are provided not in sorted order to ensure they're sorted when
    # creating the token.
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            data,
            "example",
            token_name="some-token",
            scopes=["read:all", "exec:admin"],
            expires=expires,
            ip_address="192.168.0.1",
        )
        assert await token_service.get_user_info(user_token) == user_info
        info = await token_service.get_token_info_unchecked(user_token.key)
    assert info and info == TokenInfo(
        token=user_token.key,
        username=user_info.username,
        token_name="some-token",
        token_type=TokenType.user,
        scopes=["exec:admin", "read:all"],
        created=info.created,
        last_used=None,
        expires=int(expires.timestamp()),
        parent=None,
    )
    assert_is_now(info.created)
    assert await token_service.get_data(user_token) == TokenData(
        token=user_token,
        username=user_info.username,
        token_type=TokenType.user,
        scopes=["exec:admin", "read:all"],
        created=info.created,
        expires=info.expires,
        name=user_info.name,
        uid=user_info.uid,
    )

    async with factory.session.begin():
        history = await token_service.get_change_history(
            data, token=user_token.key, username=data.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="some-token",
            scopes=["exec:admin", "read:all"],
            expires=info.expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address="192.168.0.1",
            event_time=info.created,
        )
    ]
コード例 #20
0
ファイル: analyze_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_analyze_token(client: AsyncClient,
                             factory: ComponentFactory) -> None:
    token = Token()

    # Handle with no session.
    r = await client.post("/auth/analyze", data={"token": str(token)})
    assert r.status_code == 200
    assert r.json() == {
        "handle": token.dict(),
        "token": {
            "errors": ["Invalid token"],
            "valid": False
        },
    }

    # Valid token.
    token_data = await create_session_token(factory,
                                            group_names=["foo", "bar"],
                                            scopes=["admin:token", "read:all"])
    assert token_data.expires
    assert token_data.groups
    token = token_data.token
    r = await client.post("/auth/analyze", data={"token": str(token)})

    # Check that the results from /analyze include the token components and
    # the token information.
    assert r.status_code == 200
    assert r.json() == {
        "handle": token.dict(),
        "token": {
            "data": {
                "exp": int(token_data.expires.timestamp()),
                "iat": int(token_data.created.timestamp()),
                "isMemberOf": [g.dict() for g in token_data.groups],
                "name": token_data.name,
                "scope": "admin:token read:all",
                "sub": token_data.username,
                "uid": token_data.username,
                "uidNumber": str(token_data.uid),
            },
            "valid": True,
        },
    }

    # Create a session token with minimum data.
    token_data.name = None
    token_data.uid = None
    token_data.groups = None
    token_service = factory.create_token_service()
    user_token = await token_service.create_user_token(
        token_data,
        token_data.username,
        token_name="foo",
        scopes=[],
        expires=None,
        ip_address="127.0.0.1",
    )
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data

    # Check that the correct fields are omitted and nothing odd happens.
    r = await client.post("/auth/analyze", data={"token": str(user_token)})
    assert r.status_code == 200
    assert r.json() == {
        "handle": user_token.dict(),
        "token": {
            "data": {
                "iat": int(user_token_data.created.timestamp()),
                "scope": "",
                "sub": user_token_data.username,
                "uid": user_token_data.username,
            },
            "valid": True,
        },
    }
コード例 #21
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,
        },
    ]
コード例 #22
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
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,
        )
    ]
コード例 #23
0
async def test_csrf_required(
    client: AsyncClient, factory: ComponentFactory
) -> None:
    token_data = await create_session_token(factory, scopes=["admin:token"])
    csrf = await set_session_cookie(client, token_data.token)
    token_service = factory.create_token_service()
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            token_data,
            token_data.username,
            token_name="foo",
            scopes=[],
            ip_address="127.0.0.1",
        )

    r = await client.post(
        "/auth/api/v1/tokens",
        json={"username": "******", "token_type": "service"},
    )
    assert r.status_code == 403
    r = await client.post(
        "/auth/api/v1/tokens",
        headers={"X-CSRF-Token": f"XXX{csrf}"},
        json={"username": "******", "token_type": "service"},
    )
    assert r.status_code == 403

    r = await client.post(
        "/auth/api/v1/users/example/tokens", json={"token_name": "some token"}
    )
    assert r.status_code == 403

    r = await client.post(
        "/auth/api/v1/users/example/tokens",
        headers={"X-CSRF-Token": f"XXX{csrf}"},
        json={"token_name": "some token"},
    )
    assert r.status_code == 403

    r = await client.delete(
        f"/auth/api/v1/users/example/tokens/{user_token.key}"
    )
    assert r.status_code == 403

    r = await client.delete(
        f"/auth/api/v1/users/example/tokens/{user_token.key}",
        headers={"X-CSRF-Token": f"XXX{csrf}"},
    )
    assert r.status_code == 403

    r = await client.patch(
        f"/auth/api/v1/users/example/tokens/{user_token.key}",
        json={"token_name": "some token"},
    )
    assert r.status_code == 403

    r = await client.patch(
        f"/auth/api/v1/users/example/tokens/{user_token.key}",
        headers={"X-CSRF-Token": f"XXX{csrf}"},
        json={"token_name": "some token"},
    )
    assert r.status_code == 403
コード例 #24
0
async def test_modify(
    factory: ComponentFactory,
    mock_kubernetes: MockKubernetesApi,
    caplog: LogCaptureFixture,
) -> None:
    await create_test_service_tokens(mock_kubernetes)
    kubernetes_service = factory.create_kubernetes_service(MagicMock())
    token_service = factory.create_token_service()

    # Valid secret but with a bogus token.
    secret = V1Secret(
        api_version="v1",
        kind="Secret",
        data={"token": "bogus"},
        metadata=V1ObjectMeta(name="gafaelfawr-secret", namespace="mobu"),
        type="Opaque",
    )
    await mock_kubernetes.create_namespaced_secret("mobu", secret)

    # Valid secret but with a nonexistent token.
    secret = V1Secret(
        api_version="v1",
        kind="Secret",
        data={"token": token_as_base64(Token())},
        metadata=V1ObjectMeta(
            name="gafaelfawr",
            namespace="nublado2",
            labels={
                "foo": "bar",
                "other": "blah",
            },
            annotations={
                "argocd.argoproj.io/compare-options": "IgnoreExtraneous",
                "argocd.argoproj.io/sync-options": "Prune=false",
            },
        ),
        type="Opaque",
    )
    await mock_kubernetes.create_namespaced_secret("nublado2", secret)

    # Update the secrets.  This should replace both with fresh secrets.
    await kubernetes_service.update_service_tokens()
    await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes)

    # Check the logging.
    assert parse_log(caplog) == [
        {
            "event": "Created new service token",
            "key": ANY,
            "severity": "info",
            "token_scope": "admin:token",
            "token_username": "******",
        },
        {
            "event": "Updated mobu/gafaelfawr-secret secret",
            "scopes": ["admin:token"],
            "severity": "info",
            "service": "mobu",
        },
        {
            "event": "Created new service token",
            "key": ANY,
            "severity": "info",
            "token_scope": "",
            "token_username": "******",
        },
        {
            "event": "Updated nublado2/gafaelfawr secret",
            "scopes": [],
            "severity": "info",
            "service": "nublado-hub",
        },
    ]

    # Replace one secret with a valid token for the wrong service.
    async with factory.session.begin():
        token = await token_service.create_token_from_admin_request(
            AdminTokenRequest(
                username="******",
                token_type=TokenType.service,
                scopes=["admin:token"],
            ),
            TokenData.internal_token(),
            ip_address=None,
        )
    secret = V1Secret(
        api_version="v1",
        kind="Secret",
        data={"token": token_as_base64(token)},
        metadata=V1ObjectMeta(name="gafaelfawr-secret", namespace="mobu"),
        type="Opaque",
    )
    await mock_kubernetes.replace_namespaced_secret(
        "gafaelfawr-secret", "mobu", secret
    )

    # Replace the other token with a valid token with the wrong scopes.
    async with factory.session.begin():
        token = await token_service.create_token_from_admin_request(
            AdminTokenRequest(
                username="******",
                token_type=TokenType.service,
                scopes=["read:all"],
            ),
            TokenData.internal_token(),
            ip_address=None,
        )
    secret = V1Secret(
        api_version="v1",
        kind="Secret",
        data={"token": token_as_base64(token)},
        metadata=V1ObjectMeta(name="gafaelfawr", namespace="nublado2"),
        type="Opaque",
    )
    await mock_kubernetes.replace_namespaced_secret(
        "gafaelfawr", "nublado2", secret
    )

    # Update the secrets.  This should create new tokens for both.
    await kubernetes_service.update_service_tokens()
    await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes)
    nublado_secret = await mock_kubernetes.read_namespaced_secret(
        "gafaelfawr", "nublado2"
    )

    # Finally, replace a secret with one with no token.
    secret = V1Secret(
        api_version="v1",
        data={},
        metadata=V1ObjectMeta(name="gafaelfawr-secret", namespace="mobu"),
        type="Opaque",
    )
    await mock_kubernetes.replace_namespaced_secret(
        "gafaelfawr-secret", "mobu", secret
    )

    # Update the secrets.  This should create a new token for the first secret
    # but not for the second.
    await kubernetes_service.update_service_tokens()
    await assert_kubernetes_secrets_are_correct(
        factory, mock_kubernetes, is_fresh=False
    )
    assert nublado_secret == await mock_kubernetes.read_namespaced_secret(
        "gafaelfawr", "nublado2"
    )
コード例 #25
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
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"]
コード例 #26
0
async def build_history(
    factory: ComponentFactory,
) -> List[TokenChangeHistoryEntry]:
    """Perform a bunch of token manipulations and return the history entries.

    Assume that all token manipulations generate the correct history entries,
    since that's tested in other tests.  The only point of this function is to
    build enough history that we can make interesting paginated queries of it.
    """
    token_service = factory.create_token_service()

    user_info_one = TokenUserInfo(username="******")
    async with factory.session.begin():
        token_one = await token_service.create_session_token(
            user_info_one,
            scopes=["exec:test", "read:all", "user:token"],
            ip_address="192.0.2.3",
        )
        token_data_one = await token_service.get_data(token_one)
        assert token_data_one
        await token_service.get_internal_token(
            token_data_one,
            "foo",
            scopes=["exec:test", "read:all"],
            ip_address="192.0.2.4",
        )
        internal_token_one_bar = await token_service.get_internal_token(
            token_data_one, "bar", scopes=["read:all"], ip_address="192.0.2.3"
        )
        token_data_internal_one_bar = await token_service.get_data(
            internal_token_one_bar
        )
        assert token_data_internal_one_bar
        await token_service.get_internal_token(
            token_data_internal_one_bar,
            "baz",
            scopes=[],
            ip_address="10.10.10.10",
        )
        notebook_token_one = await token_service.get_notebook_token(
            token_data_one, ip_address="198.51.100.5"
        )
        token_data_notebook_one = await token_service.get_data(
            notebook_token_one
        )
        assert token_data_notebook_one
        await token_service.get_internal_token(
            token_data_notebook_one,
            "foo",
            scopes=["exec:test"],
            ip_address="10.10.10.20",
        )

    user_info_two = TokenUserInfo(username="******")
    async with factory.session.begin():
        token_two = await token_service.create_session_token(
            user_info_two,
            scopes=["read:some", "user:token"],
            ip_address="192.0.2.20",
        )
        token_data_two = await token_service.get_data(token_two)
        assert token_data_two
        user_token_two = await token_service.create_user_token(
            token_data_two,
            token_data_two.username,
            token_name="some token",
            scopes=["read:some", "user:token"],
            ip_address="192.0.2.20",
        )
        token_data_user_two = await token_service.get_data(user_token_two)
        assert token_data_user_two
        await token_service.get_internal_token(
            token_data_user_two,
            "foo",
            scopes=["read:some"],
            ip_address="10.10.10.10",
        )
        assert await token_service.modify_token(
            user_token_two.key,
            token_data_user_two,
            token_data_user_two.username,
            ip_address="192.0.2.20",
            token_name="happy token",
        )

    async with factory.session.begin():
        request = AdminTokenRequest(
            username="******",
            token_type=TokenType.service,
            scopes=["admin:token"],
        )
        service_token = await token_service.create_token_from_admin_request(
            request,
            TokenData.bootstrap_token(),
            ip_address="2001:db8:034a:ea78:4278:4562:6578:9876",
        )
        service_token_data = await token_service.get_data(service_token)
        assert service_token_data
        assert await token_service.modify_token(
            user_token_two.key,
            service_token_data,
            ip_address="2001:db8:034a:ea78:4278:4562:6578:9876",
            scopes=["admin:token", "read:all"],
        )
        assert await token_service.modify_token(
            user_token_two.key,
            service_token_data,
            ip_address="2001:db8:034a:ea78:4278:4562:6578:af42",
            token_name="other name",
            expires=current_datetime() + timedelta(days=30),
            scopes=["read:all"],
        )
        assert await token_service.delete_token(
            token_one.key,
            service_token_data,
            username=token_data_one.username,
            ip_address="2001:db8:034a:ea78:4278:4562:6578:9876",
        )

    # Spread out the timestamps so that we can test date range queries.  Every
    # other entry has the same timestamp as the previous entry to test that
    # queries handle entries with the same timestamp.
    async with factory.session.begin():
        stmt = select(TokenChangeHistory).order_by(TokenChangeHistory.id)
        result = await factory.session.execute(stmt)
        entries = [e[0] for e in result.all()]
        event_time = current_datetime() - timedelta(seconds=len(entries) * 5)
        for i, entry in enumerate(entries):
            entry.event_time = datetime_to_db(event_time)
            if i % 2 != 0:
                event_time += timedelta(seconds=5)

    async with factory.session.begin():
        history = await token_service.get_change_history(service_token_data)
    assert history.count == 20
    assert len(history.entries) == 20
    return history.entries