Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User-level state shared accross browser (and potentially computers) #2240

Closed
aranvir opened this issue Jan 27, 2024 · 5 comments
Closed

User-level state shared accross browser (and potentially computers) #2240

aranvir opened this issue Jan 27, 2024 · 5 comments
Labels
question Question

Comments

@aranvir
Copy link
Contributor

aranvir commented Jan 27, 2024

Wave SDK Version, OS

Wave 1.0.0 (and v0.26.3)
Windows 10
Python

Actual behavior

When using q.app.user to share state for a user session, this state is actually also shared across browser instances of the same computer. There might even be the case that it is shared across computers (see h2oai/wave-apps#118 (comment))

Expected behavior

As described in https://wave.h2o.ai/docs/state, it is expected that the state is shared across tabs of a browser session (clients of the same user). Opening a different browser session (incognito mode, chrome instead of firefox, different computer) should give each user "fresh state".

Steps To Reproduce

I provided sample code below which lets you set a user name that is stored in q.user.name. I tested this with Firefox, Firefox Incognito and Chrome. In my home network, I could not figure out how to serve waved on 0.0.0.0 and running a proxy also didn't really work (probably my firewall, idk). I did test it with a browser running from WSLv2 on the same machine, accessing the app via the proxy, so that "should" be like a different computer since it's not originating from localhost. But I did not manage to test this with actually separate computers in the same network.

However, as mentioned before, @HugoP reported that the user state was shared on different computers for the same app.

The example below is with run_on but I also tested it with handle_on for version 1.0.0 and version 0.26.3 just in case.

from typing import Optional, List
from h2o_wave import main, app, Q, ui, on, data, run_on


def add_card(q: Q, name, card) -> None:
    q.client.cards.add(name)
    q.page[name] = card


# Remove all the cards related to navigation.
def clear_cards(q: Q, ignore: Optional[List[str]] = []) -> None:
    print("Clearing cards")
    if not q.client.cards:
        print("No cards")
        return

    for name in q.client.cards.copy():
        if name not in ignore:
            del q.page[name]
            q.client.cards.remove(name)


@on('#page1')
async def page1(q: Q):
    clear_cards(q)
    print("Loading page1")
    q.page['sidebar'].value = '#page1'

    add_card(q, 'user_data', ui.form_card('horizontal', items=[
        ui.text_l(f"Current name: {q.user.name}"),
        ui.textbox('enter_name', 'Enter user name'),
        ui.button('change_name', 'Change name', primary=True)
    ]))


@on('change_name')
async def change_name(q: Q):
    q.user.name = q.args.enter_name
    await page1(q)


async def init(q: Q) -> None:
    q.page['meta'] = ui.meta_card(box='', layouts=[ui.layout(breakpoint='xs', min_height='100vh', zones=[
        ui.zone('main', size='1', direction=ui.ZoneDirection.ROW, zones=[
            ui.zone('sidebar', size='250px'),
            ui.zone('body', zones=[
                ui.zone('header'),
                ui.zone('content', zones=[
                    # Specify various zones and use the one that is currently needed. Empty zones are ignored.
                    ui.zone('horizontal', size='1', direction=ui.ZoneDirection.ROW),
                    ui.zone('centered', size='1 1 1 1', align='center'),
                    ui.zone('vertical', size='1'),
                    ui.zone('grid', direction=ui.ZoneDirection.ROW, wrap='stretch', justify='center')
                ]),
            ]),
        ])
    ])])
    q.page['sidebar'] = ui.nav_card(
        box='sidebar', color='primary', title='My App', subtitle="Let's conquer the world!",
        value=f'#{q.args["#"]}' if q.args['#'] else '#page1',
        image='https://wave.h2o.ai/img/h2o-logo.svg', items=[
            ui.nav_group('Menu', items=[ui.nav_item(name='#page1', label='Home'),]),
        ])
    q.page['header'] = ui.header_card(
        box='header', title='', subtitle='',
    )
    # If no active hash present, render page1.
    if q.args['#'] is None:
        await page1(q)


@app('/')
async def serve(q: Q):
    if not q.client.initialized:
        # Run only once per client connection (e.g. new tabs by the same user).
        q.client.cards = set()
        await init(q)
        q.client.initialized = True
        q.client.new = True  # Indicate that client connected for the first time
        print("client initialized")
    if not q.user.initialized:
        q.user.initialized = True
        q.user.name = ""
        print("user initialized")

    print("ARGS BUFFER:\n", q.args)
    print("CLIENT BUFFER:\n", q.client)
    print("USER BUFFER:\n", q.user)

    await run_on(q)
    await q.page.save()
@aranvir aranvir added the bug Bug in code label Jan 27, 2024
@mturoci
Copy link
Collaborator

mturoci commented Jan 29, 2024

Hey @aranvir. q.user assumes the presence of an OIDC authenticated user (maybe our docs need improvement). You can see how it's scoped here. The example you linked uses a custom JWT auth.

@mturoci mturoci added question Question and removed bug Bug in code labels Jan 29, 2024
@aranvir
Copy link
Contributor Author

aranvir commented Jan 29, 2024

I see, thank you for the clarification. Then I implemented this JWT approach with the wrong assumption. But I also guess it's then not even possible to use user state reasonably without OIDC, right? Or rather - I assume that user state is then also shared across all users like app state.

Does it then even make sense to keep the JWT example, if it can't work?

@mturoci
Copy link
Collaborator

mturoci commented Jan 29, 2024

But I also guess it's then not even possible to use user state reasonably without OIDC, right?

You would need to implement your own version of q.user: A global dict keyed by JWT token usernames/subjects whatever. The implementation would be very similar to the one I linked above. Another option would add it natively to Wave somehow.

I assume that user state is then also shared across all users like app state.

Correct. Non-authenticated users have auth.subject set to anon. Since all users have the same key so it basically equals to q.app in that case.

Does it then even make sense to keep the JWT example, if it can't work?

I would say it does. It only can't work with q.user. The point of the example is to show that JWT is possible (login screen) if you put in the effort. In real-world apps, user-state is stored in an external DB anyway, not in-memory.

@aranvir
Copy link
Contributor Author

aranvir commented Jan 29, 2024

Thanks again for the feedback! That gives me some food for thought...

@mturoci
Copy link
Collaborator

mturoci commented Jan 29, 2024

Also, since this is a very niche feature (most folks go with OIDC), it won't be prioritized, but I am fine with accepting a contribution as long as it doesn't make the code much more complex.

Feel free to ping me in case of any more questions! Covnerting this into a GH discussion.

@h2oai h2oai locked and limited conversation to collaborators Jan 29, 2024
@mturoci mturoci converted this issue into discussion #2242 Jan 29, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question
Projects
None yet
Development

No branches or pull requests

2 participants