Skip to content
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

feat: allow running review app to validate pr from external contribution #253

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .buildpacks
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
https://github.com/FabienArcellier/nodejs-buildpack#streamsync-review
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know where to document the role of the Procfile and .buildpacks. .buildpacks does not support code comment.

https://github.com/FabienArcellier/python-buildpack#streamsync-review
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: python apps/reviewapp.py
115 changes: 115 additions & 0 deletions apps/reviewapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
This is a simple application to help our team to review contribution.

>>> python apps/reviewapp.py

Runs this application in public and requires authentication.

>>> export HOST=0.0.0.0; export BASICAUTH=admin:admin; python apps/reviewapp.py
"""
import base64
import os
FabienArcellier marked this conversation as resolved.
Show resolved Hide resolved
import time

import uvicorn
from fastapi.responses import HTMLResponse
from fastapi import FastAPI, status

import streamsync.serve

HOST = os.getenv('HOST', 'localhost')
PORT = int(os.getenv('PORT', '8000'))
print(f"listen on {HOST}:{PORT}")


def app_path(app_name: str) -> str:
return os.path.join(os.path.dirname(__file__), app_name)

root_asgi_app = FastAPI(lifespan=streamsync.serve.lifespan)
sub_asgi_app_1 = streamsync.serve.get_asgi_app(app_path("hello"), "edit", enable_remote_edit=True)
sub_asgi_app_2 = streamsync.serve.get_asgi_app(app_path("default"), "edit", enable_remote_edit=True)
sub_asgi_app_3 = streamsync.serve.get_asgi_app(app_path("quickstart"), "edit", enable_remote_edit=True)

root_asgi_app.mount("/hello/", sub_asgi_app_1)
root_asgi_app.mount("/default/", sub_asgi_app_2)
root_asgi_app.mount("/quickstart/", sub_asgi_app_3)



@root_asgi_app.get("/")
async def init():
links = [
f'<li><a href="/hello">hello</a></li>',
f'<li><a href="/default"">default</a></li>',
f'<li><a href="/quickstart">quickstart</a></li>',
]

return HTMLResponse("""
<h1>Streamsync review app</h1>
<ul>
""" + "\n".join(links) + """
</ul>
""", status_code=200)



@root_asgi_app.middleware("http")
async def valid_authentication(request, call_next):
"""
Secures access to the review application using basic auth

The username and password is stored in the BASICAUTH environment variable.
The authentication process is sequential and when it's wrong it take one second to try again. This protection
is sufficient to limit brute force attack.
"""
if HOST == 'localhost':
"""
Locally, you can launch the review application without needing to authenticate.

The application bypass the authentication middleware.
"""
return await call_next(request)

_auth = request.headers.get('Authorization')
if not check_permission(_auth):
return HTMLResponse("", status.HTTP_401_UNAUTHORIZED, {"WWW-Authenticate": "Basic"})
return await call_next(request)


def check_permission(auth) -> bool:
"""
Secures access to the review application using basic auth

>>> is_valid_token = check_permission('Basic dXNlcm5hbWU6cGFzc3dvcmQ=')
"""
if auth is None:
return False

scheme, data = (auth or ' ').split(' ', 1)
if scheme != 'Basic':
return False

username, password = base64.b64decode(data).decode().split(':', 1)
basicauth = os.getenv('BASICAUTH')
if auth is None:
raise ValueError('BASICAUTH environment variable is not set')

basicauth_part = basicauth.split(':')
if len(basicauth_part) != 2:
raise ValueError('BASICAUTH environment variable is not set')

basicauth_username, basicauth_password = basicauth_part

time.sleep(1)
if username == basicauth_username and password == basicauth_password:
return True
else:
time.sleep(1)
return False


uvicorn.run(root_asgi_app,
host=HOST,
port=PORT,
log_level="warning",
ws_max_size=streamsync.serve.MAX_WEBSOCKET_MESSAGE_SIZE)
108 changes: 107 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ watchdog = ">= 3.0.0, < 4"
pandas = {version = ">= 2.2.0, < 3", optional = true}
pyarrow = {version = ">= 15.0.0, < 16.0.0",optional = true}
plotly = {version = ">= 5.18.0, < 6", optional = true}
scikit-learn = {version = "^1.4.1.post1", optional = true}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this here? Do we require it now for any reason?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using it in the getting started application.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you're right, and this doesn't bloat the standard distributions (bare, ds) right? Just the build



[tool.poetry.group.build]
Expand Down
1 change: 1 addition & 0 deletions src/streamsync/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ async def stream(websocket: WebSocket):

server_path = os.path.dirname(__file__)
server_static_path = pathlib.Path(server_path) / "static"

if server_static_path.exists():
asgi_app.mount(
"/", StaticFiles(directory=str(server_static_path), html=True), name="server_static")
Expand Down
Loading