Skip to content

Commit

Permalink
cleaned up source code
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Piskun <[email protected]>
  • Loading branch information
bigcat88 committed Jul 4, 2024
1 parent 786fa0e commit 7b61b84
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 83 deletions.
10 changes: 2 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,8 @@ build-push:
docker login ghcr.io
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag ghcr.io/cloud-py-api/windmill_app_dev:latest .

.PHONY: build-dev
build-dev:
docker login ghcr.io
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag ghcr.io/cloud-py-api/windmill_app_dev:latest .
#docker build -t windmill_app_dev:latest .

.PHONY: run-dev
run-dev:
.PHONY: run
run:
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister windmill_app --silent --force || true
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register windmill_app --force-scopes \
--info-xml https://raw.githubusercontent.com/cloud-py-api/windmill_app/main/appinfo/info.xml
Expand Down
240 changes: 167 additions & 73 deletions ex_app/lib/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"""Windmill as ExApp"""
"""Windmill as an ExApp"""

import contextlib
import json
import os
import random
import string
import typing
from base64 import b64decode
from contextlib import asynccontextmanager
from pathlib import Path

import httpx
from fastapi import BackgroundTasks, Depends, FastAPI, Request, responses
from fastapi import BackgroundTasks, Depends, FastAPI, Request, responses, status
from nc_py_api import NextcloudApp
from nc_py_api.ex_app import LogLvl, nc_app, run_app
from nc_py_api.ex_app import LogLvl, nc_app, persistent_storage, run_app
from nc_py_api.ex_app.integration_fastapi import fetch_models_task
from starlette.responses import FileResponse, Response

Expand All @@ -18,6 +23,78 @@
# os.environ["APP_SECRET"] = "12345"
# os.environ["APP_PORT"] = "23000"

USERS_STORAGE_PATH = Path(persistent_storage()).joinpath("windmill_users_config.json")
USERS_STORAGE = {}
print(str(USERS_STORAGE_PATH), flush=True)
if USERS_STORAGE_PATH.exists():
with open(USERS_STORAGE_PATH, encoding="utf-8") as __f:
USERS_STORAGE.update(json.load(__f))


def add_user_to_storage(user_name: str, password: str, token: str = "") -> None:
USERS_STORAGE[user_name] = {"password": password, "token": token}
with open(USERS_STORAGE_PATH, "w", encoding="utf-8") as f:
json.dump(USERS_STORAGE, f, indent=4)


async def create_user(user_name: str) -> str:
password = generate_random_string()
async with httpx.AsyncClient() as client:
r = await client.request(
method="POST",
url="http://127.0.0.1:8000/api/users/create",
json={
"email": f"{user_name}@windmill.dev",
"password": password,
"super_admin": True,
"name": user_name,
},
cookies={"token": USERS_STORAGE["[email protected]"]["token"]},
)
r = await client.post(
url="http://127.0.0.1:8000/api/auth/login",
json={"email": f"{user_name}@windmill.dev", "password": password},
)
add_user_to_storage(user_name, password, r.text)
return r.text


async def login_user(user_name: str, password: str) -> str:
async with httpx.AsyncClient() as client:
r = await client.post(
url="http://127.0.0.1:8000/api/auth/login",
json={"email": f"{user_name}@windmill.dev", "password": password},
)
if r.status_code >= 400:
raise RuntimeError(f"login_user: {r.text}")
return r.text


async def check_token(token: str) -> bool:
async with httpx.AsyncClient() as client:
r = await client.get("http://127.0.0.1:8000/api/users/whoami", cookies={"token": token})
return bool(r.status_code < 400)


async def provision_user(request: Request) -> None:
if "token" in request.cookies:
print(f"DEBUG: TOKEN IS PRESENT: {request.cookies['token']}", flush=True)
if (await check_token(request.cookies["token"])) is True:
return
print("DEBUG: TOKEN IS INVALID", flush=True)

user_name = get_windmill_username_from_request(request)
if user_name in USERS_STORAGE:
zzz = USERS_STORAGE[user_name]["token"]
aaa = await check_token(zzz)
if not USERS_STORAGE[user_name]["token"] or aaa is False:
user_password = USERS_STORAGE[user_name]["password"]
add_user_to_storage(user_name, user_password, await login_user(user_name, user_password))
else:
await create_user(user_name)
request.cookies["token"] = USERS_STORAGE[user_name]["token"]
print(f"DEBUG: ADDING TOKEN({request.cookies['token']}) to request", flush=True)


@asynccontextmanager
async def lifespan(app: FastAPI): # pylint: disable=unused-argument
Expand All @@ -28,15 +105,26 @@ async def lifespan(app: FastAPI): # pylint: disable=unused-argument
# APP.add_middleware(AppAPIAuthMiddleware) # set global AppAPI authentication middleware


def get_windmill_username_from_request(request: Request) -> str:
auth_aa = b64decode(request.headers.get("AUTHORIZATION-APP-API", "")).decode("UTF-8")
try:
username, _ = auth_aa.split(":", maxsplit=1)
except ValueError:
username = ""
if not username:
raise RuntimeError("`username` should be always set.")
return "wapp_" + username


def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
print(f"enabled={enabled}")
if enabled:
nc.log(LogLvl.WARNING, f"Hello from {nc.app_cfg.app_name} :)")
nc.ui.resources.set_script("top_menu", "windmill_app", "js/windmill_app-main")
nc.ui.top_menu.register("windmill_app", "Workflow Engine", "img/app.svg")
nc.ui.resources.set_script("top_menu", "windmill_app", "ex_app/js/windmill_app-main")
nc.ui.top_menu.register("windmill_app", "Workflow Engine", "ex_app/img/app.svg")
else:
nc.log(LogLvl.WARNING, f"Bye bye from {nc.app_cfg.app_name} :(")
nc.ui.resources.delete_script("top_menu", "windmill_app", "js/windmill_app-main")
nc.ui.resources.delete_script("top_menu", "windmill_app", "ex_app/js/windmill_app-main")
nc.ui.top_menu.unregister("windmill_app")
return ""

Expand All @@ -59,96 +147,102 @@ def enabled_callback(enabled: bool, nc: typing.Annotated[NextcloudApp, Depends(n

@APP.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"])
async def proxy_backend_requests(request: Request, path: str):
print(f"proxy_BACKEND_requests: {path} - {request.method}\nCookies: {request.cookies}", flush=True)
# print(f"proxy_BACKEND_requests: {path} - {request.method}\nCookies: {request.cookies}", flush=True)
await provision_user(request)
async with httpx.AsyncClient() as client:
url = f"http://127.0.0.1:8000/api/{path}"
headers = {key: value for key, value in request.headers.items() if key.lower() != "host"}
# print(f"proxy_BACKEND_requests: method={request.method}, path={path}, status={response.status_code}")
response = await client.request(
method=request.method,
url=url,
params=request.query_params,
headers=headers,
cookies=request.cookies,
content=await request.body(),
)
print(
f"proxy_BACKEND_requests: method={request.method}, path={path}, status={response.status_code}", flush=True
)

headers = {key: value for key, value in request.headers.items() if key.lower() not in ("host", "cookie")}
if request.method == "GET":
response = await client.get(
url,
params=request.query_params,
cookies=request.cookies,
headers=headers,
)
else:
response = await client.request(
method=request.method,
url=url,
params=request.query_params,
headers=headers,
cookies=request.cookies,
content=await request.body(),
)
# print(
# f"proxy_BACKEND_requests: method={request.method}, path={path}, status={response.status_code}", flush=True
# )
response_header = dict(response.headers)
response_header.pop("transfer-encoding", None)
response_to_nc = Response(content=response.content, status_code=response.status_code, headers=response_header)
# TO-DO: here maybe it is not needed?
abc = response.cookies
for cookie in abc:
response_to_nc.set_cookie(key=cookie[0], value=cookie[1])

# TO-DO: here maybe it is not needed?
response_to_nc.headers["content-security-policy"] = "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
response_to_nc.headers["Access-Control-Allow-Origin"] = "*"
response_to_nc.headers["X-Permitted-Cross-Domain-Policies"] = "all"
return response_to_nc
return Response(content=response.content, status_code=response.status_code, headers=response_header)


@APP.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"])
async def proxy_frontend_requests(request: Request, path: str):
print(f"proxy_FRONTEND_requests: {path} - {request.method}\nCookies: {request.cookies}", flush=True)

# 2024-06-13 10:27:55 proxy_FRONTEND_requests: index.php/apps/app_api/proxy/windmill_app/ - GET

await provision_user(request)
if path == "index.php/apps/app_api/proxy/windmill_app/":
path = path.replace("index.php/apps/app_api/proxy/windmill_app/", "")
if path.startswith(("img/", "js/")):
# file_server_path = Path("../" + path)
file_server_path = Path("/ex_app/" + path)
if path.startswith("ex_app"):
file_server_path = Path("../../" + path)
elif not path or path == "user/login":
# file_server_path = Path("../../windmill_tmp/frontend/build/200.html")
file_server_path = Path("/iframe/200.html")
else:
# file_server_path = Path("../../windmill_tmp/frontend/build/").joinpath(path)
file_server_path = Path("/iframe/").joinpath(path)
if file_server_path.exists():
media_type = None
if str(file_server_path).endswith(".js"):
media_type = "application/javascript"
response = FileResponse(str(file_server_path), media_type=media_type)
response.headers["content-security-policy"] = "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["X-Permitted-Cross-Domain-Policies"] = "all"
print("proxy_FRONTEND_requests: <OK> Returning: ", str(file_server_path), flush=True)
return response

print("proxy_FRONTEND_requests: <BAD> FILE DOES NOT EXIST: ", file_server_path, flush=True)
print("proxy_FRONTEND_TO_BACKEND_requests: Asking for reply from BACKEND..", flush=True)
async with httpx.AsyncClient() as client:
url = f"http://127.0.0.1:8000/{path}"
headers = {key: value for key, value in request.headers.items() if key.lower() != "host"}
response = await client.request(
method=request.method,
url=url,
params=request.query_params,
headers=headers,
cookies=request.cookies,
content=await request.body(),
if not file_server_path.exists():
return Response(status_code=status.HTTP_404_NOT_FOUND)
response = FileResponse(str(file_server_path))
response.headers["content-security-policy"] = "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
print("proxy_FRONTEND_requests: <OK> Returning: ", str(file_server_path), flush=True)
return response


def initialize_windmill() -> None:
if not USERS_STORAGE_PATH.exists():
while True: # Let's wait until Windmill opens the port.
with contextlib.suppress(httpx.ReadError, httpx.ConnectError, httpx.RemoteProtocolError):
r = httpx.get("http://127.0.0.1:8000/api/users/whoami")
if r.status_code in (401, 403):
break
r = httpx.post(
url="http://127.0.0.1:8000/api/auth/login", json={"email": "[email protected]", "password": "changeme"}
)
print(
f"proxy_FRONTEND_TO_BACKEND_requests: method={request.method}, path={path}, status={response.status_code}",
flush=True,
if r.status_code >= 400:
raise RuntimeError(f"initialize_windmill: can not login with default credentials, {r.text}")
default_token = r.text
new_default_password = generate_random_string()
r = httpx.post(
url="http://127.0.0.1:8000/api/users/setpassword",
json={"password": new_default_password},
cookies={"token": default_token},
)
response_to_nc = Response(
content=response.content, status_code=response.status_code, headers=dict(response.headers)
if r.status_code >= 400:
raise RuntimeError(f"initialize_windmill: can not change default credentials password, {r.text}")
add_user_to_storage("[email protected]", new_default_password, default_token)
r = httpx.post(
url="http://127.0.0.1:8000/api/workspaces/create",
json={"id": "nextcloud", "name": "nextcloud"},
cookies={"token": default_token},
)
abc = response.cookies
for cookie in abc:
response_to_nc.set_cookie(key=cookie[0], value=cookie[1])
response_to_nc.headers["content-security-policy"] = "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;"
response_to_nc.headers["Access-Control-Allow-Origin"] = "*"
response_to_nc.headers["X-Permitted-Cross-Domain-Policies"] = "all"
return response_to_nc
if r.status_code >= 400:
raise RuntimeError(f"initialize_windmill: can not create default workspace, {r.text}")
r = httpx.post(
url="http://127.0.0.1:8000/api/w/nextcloud/workspaces/edit_auto_invite",
json={"operator": False, "invite_all": True, "auto_add": True},
cookies={"token": default_token},
)
if r.status_code >= 400:
raise RuntimeError(f"initialize_windmill: can not create default workspace, {r.text}")


def generate_random_string(length=10):
letters = string.ascii_letters + string.digits # You can include other characters if needed
return "".join(random.choice(letters) for i in range(length)) # noqa


if __name__ == "__main__":
initialize_windmill()
# Current working dir is set for the Service we are wrapping, so change we first for ExApp default one
os.chdir(Path(__file__).parent)
run_app(APP, log_level="info") # Calling wrapper around `uvicorn.run`.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ preview = true
[tool.ruff]
line-length = 120
target-version = "py310"
select = ["A", "B", "C", "D", "E", "F", "G", "I", "S", "SIM", "PIE", "Q", "RET", "RUF", "UP" , "W"]
extend-ignore = ["D101", "D102", "D103", "D105", "D107", "D203", "D213", "D401", "I001", "RUF100", "D400", "D415"]
lint.select = ["A", "B", "C", "D", "E", "F", "G", "I", "S", "SIM", "PIE", "Q", "RET", "RUF", "UP" , "W"]
lint.extend-ignore = ["D101", "D102", "D103", "D105", "D107", "D203", "D213", "D401", "I001", "RUF100", "D400", "D415"]

[tool.isort]
profile = "black"
Expand Down

0 comments on commit 7b61b84

Please sign in to comment.