From 5b41853025e9b690a9748f5c2ce4d68597a0f6ee Mon Sep 17 00:00:00 2001 From: Douwe M Osinga Date: Fri, 8 Nov 2024 14:20:02 +0100 Subject: [PATCH 1/6] Check the other thing --- README.md | 10 +++++++++- server/users.py | 11 ++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ed15da8..9a862f9 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,19 @@ The easiest way to get started is to use the docker image. This will give you a ```shell +<<<<<<< Updated upstream docker-compose up -d +======= +docker compose up -d +docker logs neptyne-spreadsheet-neptyne-1 +>>>>>>> Stashed changes ``` -Only need to build it once. After that, you can just run `docker-compose up`. +The second statement will print out the shared secret you need to connect to the Neptyne server. +Open that url and you are in business. + +```shell ### Method 2: pip install diff --git a/server/users.py b/server/users.py index d764e96..06e5667 100644 --- a/server/users.py +++ b/server/users.py @@ -1,10 +1,12 @@ from typing import Any, Literal +from jwt import PyJWTError from sqlalchemy import func from sqlalchemy.orm import Session from tornado import web from tornado_sqlalchemy import SessionMixin +from server.gsheet_auth import decode_gsheet_extension_token from server.models import ( EmailShare, FirebaseUser, @@ -19,7 +21,7 @@ def token_from_headers(request_handler: web.RequestHandler) -> str | None: header = request_handler.request.headers.get("Authorization") if not header: - return None + return request_handler.request.headers.get("X-Neptyne-GSheet-Auth-Token") parts = header.split(" ") if len(parts) != 2 or parts[0].lower() != "bearer": @@ -55,8 +57,11 @@ async def _authenticate_request( ) if not token: raise web.HTTPError(401, "Missing token") - if not token == shared_secret: # TODO: also check for a signed token - raise web.HTTPError(401, "Invalid token") + if not token == shared_secret: + try: + decode_gsheet_extension_token(token) + except PyJWTError: + raise web.HTTPError(401, "Invalid token") return await load_user( session, "", From 721c61b1132a3e50d86cd2b7fa1ac925e9786dab Mon Sep 17 00:00:00 2001 From: Douwe M Osinga Date: Fri, 8 Nov 2024 14:28:47 +0100 Subject: [PATCH 2/6] tests --- .../neptyne-container/NeptyneModals.test.tsx | 196 ------------------ neptyne_kernel/neptyne_api/geo.py | 2 + 2 files changed, 2 insertions(+), 196 deletions(-) delete mode 100644 frontend/src/neptyne-container/NeptyneModals.test.tsx diff --git a/frontend/src/neptyne-container/NeptyneModals.test.tsx b/frontend/src/neptyne-container/NeptyneModals.test.tsx deleted file mode 100644 index 25da01e..0000000 --- a/frontend/src/neptyne-container/NeptyneModals.test.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, { MutableRefObject, ReactElement, useEffect } from "react"; -import { act, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { - ModalDispatch, - ModalReducerAction, - ModalReducerShowProps, - NeptyneModals, - useModalDispatch, -} from "./NeptyneModals"; -import noop from "lodash/noop"; -import { NoteDialog } from "../components/ToolbarControls/NoteDialog"; -import { LinkDialog } from "../components/ToolbarControls/LinkDialog"; -import { ShareDialogDataWrapper } from "../ShareDialog/ShareDialogDataWrapper"; -import { - AllowAnonymous, - OpenTyneDialogDataWrapper, -} from "../components/OpenDialog/OpenTyneDialogDataWrapper"; -import { WidgetRegistry } from "../NeptyneProtocol"; -import { AutocompleteResponse } from "../notebook/NotebookCellEditor/types"; -import { User } from "../user-context"; - -const TESTED_MODALS = [ - [OpenTyneDialogDataWrapper, screen.findAllByText.bind(screen, "Test Tyne")], - [ShareDialogDataWrapper, screen.findByText.bind(screen, 'Share "Test Tyne"')], - [NoteDialog, null], - [LinkDialog, null], -] as [ModalReducerShowProps["element"], (() => Promise) | null][]; - -const MOCK_USER = { displayName: "Me", email: "me@meptyne.com" } as User; - -const MOCK_MODAL_PROPS = { - onToggle: noop, - currentCellAttributes: {}, - onCellAttributeChange: noop, - onUpdateCellValues: noop, - widgetRegistry: {} as WidgetRegistry, - sheetSelection: { - start: { - row: 0, - col: 0, - }, - end: { - row: 0, - col: 0, - }, - }, - currentSheet: 0, - getAutocomplete: noop as () => Promise, - getWidgetState: noop as () => Promise<{ [name: string]: string }>, - validateWidgetParams: noop as () => Promise<{ [name: string]: string }>, - onCreateFunctionSubmit: noop, - user: MOCK_USER, - allowAnonymous: "no" as AllowAnonymous, - errorMessage: null, - notificationMessage: null, - onTyneAction: noop, - tyneId: "test_tyne", - tyneName: "Test Tyne", -}; - -jest.mock("../authenticatedFetch", () => { - return async function authenticatedFetch( - _: unknown, - info: Parameters[0] - ) { - const path = info.toString(); - return { - async json() { - if (path === "/api/tyne_list") - return { - tynes: [ - { - access: "EDIT", - categories: ["editableByMe", "authoredByMe"], - fileName: "test", - galleryScreenshotUrl: null, - lastModified: "2022-08-26T16:00:13.433923", - name: "Test Tyne", - owner: "Test User", - ownerColor: "#e8f5a3", - ownerProfileImage: null, - }, - ], - }; - else if (path === `/api/tynes/${MOCK_MODAL_PROPS.tyneId}/share`) - return { - shares: [], - users: [], - published: false, - }; - return {}; - }, - }; - }; -}); - -const ModalContextPortal = React.forwardRef((props, ref) => { - const modalDispatch = useModalDispatch(); - - useEffect(() => { - if (typeof ref === "function") ref(modalDispatch); - else if ("current" in ref!) ref.current = modalDispatch; - }, [ref, modalDispatch]); - - return null; -}); - -describe("NeptyneModals", () => { - describe.each([ - [ - "ref", - (ref: MutableRefObject) => ( - - ), - ], - [ - "context", - (ref: MutableRefObject) => ( - - - - ), - ], - ])("NeptyneModals open via %s", (_, modalContextRenderer) => { - it.each(TESTED_MODALS)("Opens %p", async (Modal, waitFor) => { - const modalDispatch: MutableRefObject = { current: null }; - render( - ( - modalContextRenderer as ( - ref: MutableRefObject - ) => ReactElement - )(modalDispatch) - ); - - expect(typeof modalDispatch.current).toBe("function"); - // Haven't found better generic solution yet - expect(document.querySelector(".MuiModal-root")).toBeNull(); - - await act(async () => { - modalDispatch.current?.({ - action: ModalReducerAction.Show, - props: { - element: Modal as typeof LinkDialog, - }, - }); - }); - await waitFor?.(); - - expect(document.querySelector(".MuiModal-root")).toBeInstanceOf(HTMLElement); - - await act(() => { - modalDispatch.current?.({ - action: ModalReducerAction.Hide, - }); - }); - - expect(document.querySelector(".MuiModal-root")).toBeNull(); - }); - }); - - describe.each([ - [ - "backdrop click", - async () => - await userEvent.click(document.querySelector(".MuiDialog-container")!), - ], - [ - "esc press", - async () => - await userEvent.type(document.querySelector(".MuiDialog-container")!, "{esc}"), - ], - ])("NeptyneModals close via %s", (_, handleClose) => { - it.each(TESTED_MODALS)("Closes %p", async (Modal, waitFor) => { - const modalDispatch: MutableRefObject = { current: null }; - render(); - await act(() => { - modalDispatch.current?.({ - action: ModalReducerAction.Show, - props: { - element: Modal as typeof LinkDialog, - }, - }); - }); - await waitFor?.(); - - expect(document.querySelector(".MuiModal-root")).toBeInstanceOf(HTMLElement); - - await act(async () => { - await (handleClose as () => void)(); - }); - - expect(document.querySelector(".MuiModal-root")).toBeNull(); - }); - }); -}); diff --git a/neptyne_kernel/neptyne_api/geo.py b/neptyne_kernel/neptyne_api/geo.py index f3044b4..372f9b9 100644 --- a/neptyne_kernel/neptyne_api/geo.py +++ b/neptyne_kernel/neptyne_api/geo.py @@ -1,6 +1,7 @@ from functools import partial, wraps from typing import Any, Callable + import geopandas import numpy as np import pyproj @@ -99,6 +100,7 @@ def dataset(name_or_url: str) -> GeoDataFrame: path = geopandas.datasets.get_path(name_or_url) else: import geoplot + try: path = geoplot.datasets.get_path(name_or_url) except ValueError: From e995d8647c532611e45b8e508ef4b2a303e897b9 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Fri, 8 Nov 2024 11:53:32 -0500 Subject: [PATCH 3/6] Prefer the gsheet token --- server/kernels/kernel_handlers.py | 16 ++++++++-------- server/users.py | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/server/kernels/kernel_handlers.py b/server/kernels/kernel_handlers.py index 64deab7..e6db109 100644 --- a/server/kernels/kernel_handlers.py +++ b/server/kernels/kernel_handlers.py @@ -116,7 +116,14 @@ async def init_connection( with self.make_session() as session: user: User | NonUser self.user_profile_image = "" - if auth_token: + if gsheet_auth_token: + assert gsheet_auth_token + user = NonUser.GSHEET + claims = decode_gsheet_extension_token(gsheet_auth_token) + self.user_id = None + self.user_name = f"anon-{claims.sheet_id}" + self.user_email = f"{self.user_name}@example.com" + else: user = await authenticate_request(self, session, token=auth_token) self.user_email = user.email self.user_name = user.name @@ -124,13 +131,6 @@ async def init_connection( self.allow_other_gsheets = bool( self.user_email ) and self.user_email.endswith("@neptyne.com") - else: - assert gsheet_auth_token - user = NonUser.GSHEET - claims = decode_gsheet_extension_token(gsheet_auth_token) - self.user_id = None - self.user_name = f"anon-{claims.sheet_id}" - self.user_email = f"{self.user_name}@example.com" tyne_proxy = await self.tyne_contents_manager.get( self.tyne_id, session, user, gsheet_auth_token diff --git a/server/users.py b/server/users.py index 06e5667..780d59c 100644 --- a/server/users.py +++ b/server/users.py @@ -19,9 +19,10 @@ def token_from_headers(request_handler: web.RequestHandler) -> str | None: + header = request_handler.request.headers.get("X-Neptyne-GSheet-Auth-Token") + if header: + return header header = request_handler.request.headers.get("Authorization") - if not header: - return request_handler.request.headers.get("X-Neptyne-GSheet-Auth-Token") parts = header.split(" ") if len(parts) != 2 or parts[0].lower() != "bearer": From 79ee52db0546bdf5473953f9383fbd8fd181b064 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Fri, 8 Nov 2024 11:55:24 -0500 Subject: [PATCH 4/6] fix conflict --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 9a862f9..48198f9 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,8 @@ The easiest way to get started is to use the docker image. This will give you a ```shell -<<<<<<< Updated upstream -docker-compose up -d -======= docker compose up -d docker logs neptyne-spreadsheet-neptyne-1 ->>>>>>> Stashed changes ``` From 91c2f96bce95c34712918a037fadd7776b787e61 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Fri, 8 Nov 2024 11:56:52 -0500 Subject: [PATCH 5/6] ruff --- neptyne_kernel/neptyne_api/geo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neptyne_kernel/neptyne_api/geo.py b/neptyne_kernel/neptyne_api/geo.py index 372f9b9..1290b4d 100644 --- a/neptyne_kernel/neptyne_api/geo.py +++ b/neptyne_kernel/neptyne_api/geo.py @@ -1,7 +1,6 @@ from functools import partial, wraps from typing import Any, Callable - import geopandas import numpy as np import pyproj From bf4e7cbfd5ee1b0efd5f35620cd920ff2851c6a1 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Tue, 12 Nov 2024 09:47:43 -0500 Subject: [PATCH 6/6] Set to None if no credentials can be found --- neptyne_kernel/dash.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/neptyne_kernel/dash.py b/neptyne_kernel/dash.py index 0c197f5..674ec24 100644 --- a/neptyne_kernel/dash.py +++ b/neptyne_kernel/dash.py @@ -599,6 +599,8 @@ def _discover_google_credentials(self): return self._load_authorized_credentials(authorized_user) elif (oauth_credentials := config_path / "oauth_credentials.json").exists(): return self._load_oauth_credentials(oauth_credentials) + else: + self._google_credentials = None def _authorize_service_account(self, credentials: Path): from google.oauth2.service_account import Credentials