Skip to content
Merged
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
3 changes: 1 addition & 2 deletions backend/druks/accounts/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from starlette.requests import HTTPConnection

from druks.accounts.context import current_account_id, current_conversation_id
from druks.accounts.enums import Role
from druks.accounts.exceptions import (
AuthConfigurationError,
InvalidAssertionError,
Expand Down Expand Up @@ -172,7 +171,7 @@ async def current_session_account(request: Request, session: SessionDep) -> Asyn

async def current_admin_account(account: Account = Depends(current_session_account)) -> Account:
"""The signed-in admin, never a bearer."""
if account.role == Role.ADMIN:
if account.is_admin:
return account
raise HTTPException(
status_code=403,
Expand Down
4 changes: 4 additions & 0 deletions backend/druks/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class Account(Model, Uuid7Pk, ordering=("created_at", "id")):
is_tag_required: Mapped[bool] = mapped_column(default=False, server_default=text("false"))
created_at: Mapped[datetime] = mapped_column(default=Base.utc_now)

@property
def is_admin(self) -> bool:
return self.role == Role.ADMIN

@classmethod
async def get_default(cls, session: AsyncSession) -> "Account | None":
"""The unattended account, or None before account setup."""
Expand Down
44 changes: 18 additions & 26 deletions backend/druks/browser/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from druks.accounts.dependencies import (
current_account,
current_session_account,
current_admin_account,
require_operator,
)
from druks.accounts.models import Account
Expand Down Expand Up @@ -61,12 +61,11 @@ async def list_browser_sessions(session: SessionDep, account: Account = Depends(
return sessions


@router.put("/{name}/state", status_code=204)
@router.put("/{name}/state", status_code=204, dependencies=[Depends(current_admin_account)])
async def upload_state(
name: str,
payload_format: Annotated[BrowserSessionPayloadFormat, Query(alias="payloadFormat")],
request: Request,
account: Account = Depends(current_session_account),
) -> None:
if declaration := browser_sessions.get(name):
if declaration.anonymous:
Expand All @@ -88,11 +87,8 @@ async def upload_state(
raise exceptions.BrowserSessionUnknownError(name)


@router.post("/{name}/login-window", status_code=204)
async def open_login_window(
name: str,
account: Account = Depends(current_session_account),
) -> None:
@router.post("/{name}/login-window", status_code=204, dependencies=[Depends(current_admin_account)])
async def open_login_window(name: str) -> None:
if declaration := browser_sessions.get(name):
if declaration.anonymous:
raise exceptions.BrowserSessionAnonymousError(name)
Expand All @@ -108,42 +104,38 @@ async def login_window_socket(websocket: WebSocket, name: str) -> None:
return
try:
async with session_scope(websocket.app.state.engine) as session:
await require_operator(session, websocket)
account = await require_operator(session, websocket)
window = await LoginWindow.get_for_session(name)
except (HTTPException, exceptions.BrowserApiError):
await websocket.close(code=1008)
return
if not account.is_admin:
await websocket.close(code=1008)
return
try:
await window.stream(websocket)
except Exception:
await websocket.close(code=1011)


@router.post("/{name}/login-window/save", status_code=204)
async def save_login_window(
session: SessionDep,
name: str,
account: Account = Depends(current_session_account),
) -> None:
@router.post(
"/{name}/login-window/save", status_code=204, dependencies=[Depends(current_admin_account)]
)
async def save_login_window(session: SessionDep, name: str) -> None:
window = await LoginWindow.get_for_session(name)
await window.save(session)


@router.post("/{name}/login-window/cancel", status_code=204)
async def cancel_login_window(
name: str,
account: Account = Depends(current_session_account),
) -> None:
@router.post(
"/{name}/login-window/cancel", status_code=204, dependencies=[Depends(current_admin_account)]
)
async def cancel_login_window(name: str) -> None:
window = await LoginWindow.get_for_session(name)
await window.cancel()


@router.delete("/{name}", status_code=204)
async def delete_browser_session(
session: SessionDep,
name: str,
account: Account = Depends(current_session_account),
) -> None:
@router.delete("/{name}", status_code=204, dependencies=[Depends(current_admin_account)])
async def delete_browser_session(session: SessionDep, name: str) -> None:
if row := await StoredBrowserSession.get_for_name(session, name):
await row.delete()
return
Expand Down
28 changes: 25 additions & 3 deletions backend/druks/chat/bots/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from fastapi import APIRouter, Depends, HTTPException, Path

from druks.accounts.dependencies import current_account, current_session_account
from druks.accounts.dependencies import (
current_account,
current_admin_account,
current_session_account,
)
from druks.accounts.models import Account
from druks.api.dependencies import SessionDep
from druks.apps.loader import get_app
Expand Down Expand Up @@ -47,13 +51,31 @@ async def resume_conversation(
response_model_by_alias=True,
)
async def open_one_time_code(
session: SessionDep,
connection_id: str,
account: Account = Depends(current_admin_account),
) -> OneTimeCodeResponse:
"""Open a code that makes its sender the public number's admin."""
connection = await get_bot_connection(session, connection_id)
if connection and get_app(connection.identity["app"]).bot.access == BotAccess.PUBLIC:
code = await create_one_time_code(connection, account.id)
return OneTimeCodeResponse(code=code, expires_in=ONE_TIME_CODE_TTL_SECONDS)
raise HTTPException(404, "Connection not found.")


@router.post(
"/connections/{connection_id}/pairing",
response_model=OneTimeCodeResponse,
response_model_by_alias=True,
)
async def pair(
session: SessionDep,
connection_id: str,
account: Account = Depends(current_session_account),
) -> OneTimeCodeResponse:
"""A one-time code that pairs a phone, or makes its sender the number's admin."""
"""Open a code that pairs its sender with the caller's account."""
connection = await get_bot_connection(session, connection_id)
if connection:
if connection and get_app(connection.identity["app"]).bot.access == BotAccess.PRIVATE:
code = await create_one_time_code(connection, account.id)
return OneTimeCodeResponse(code=code, expires_in=ONE_TIME_CODE_TTL_SECONDS)
raise HTTPException(404, "Connection not found.")
Expand Down
15 changes: 10 additions & 5 deletions backend/druks/chat/channels/calls/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi import APIRouter, Body, Depends, HTTPException
from sqlalchemy import Row

from druks.accounts.dependencies import current_session_account
from druks.accounts.dependencies import current_admin_account, current_session_account
from druks.accounts.enums import AccountKind
from druks.accounts.models import Account
from druks.api.dependencies import SessionDep
Expand Down Expand Up @@ -36,7 +36,11 @@ async def list_unlinked_numbers(session: SessionDep) -> list[dict]:


@router.post(
"/numbers", status_code=201, response_model=LinkedNumberResponse, response_model_by_alias=True
"/numbers",
status_code=201,
response_model=LinkedNumberResponse,
response_model_by_alias=True,
dependencies=[Depends(current_admin_account)],
)
async def link_number(
session: SessionDep,
Expand All @@ -63,7 +67,9 @@ async def link_number(
return await Twilio.link(session, owner, app=app, sid=sid, calls_url=calls_url)


@router.delete("/numbers/{number_id}", status_code=204)
@router.delete(
"/numbers/{number_id}", status_code=204, dependencies=[Depends(current_admin_account)]
)
async def remove_number(session: SessionDep, number_id: str) -> None:
if connection := await Twilio.get_connection(session, number_id):
# Removing is idempotent: a second delete finds the number removed.
Expand All @@ -77,8 +83,7 @@ async def remove_number(session: SessionDep, number_id: str) -> None:
"/numbers/{number_id}/calls", response_model=list[CallResponse], response_model_by_alias=True
)
async def list_calls(session: SessionDep, number_id: str) -> list[Row]:
"""A number's calls. The number's bot account owns them, and any operator reads them,
as any operator manages an app's numbers."""
"""A number's calls. Its bot account owns them, and any operator can read them."""
if await Twilio.get_connection(session, number_id):
return await CallsChannel.list_calls(session, number_id)
raise HTTPException(404, "Number not found.")
Expand Down
17 changes: 13 additions & 4 deletions backend/druks/chat/channels/whatsapp/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from fastapi import APIRouter, Body, Depends, HTTPException

from druks.accounts.dependencies import current_session_account
from druks.accounts.dependencies import current_admin_account, current_session_account
from druks.accounts.enums import AccountKind
from druks.accounts.models import Account
from druks.api.dependencies import SessionDep
Expand Down Expand Up @@ -34,7 +34,11 @@ async def list_sessions(


@router.post(
"/sessions", status_code=201, response_model=SessionResponse, response_model_by_alias=True
"/sessions",
status_code=201,
response_model=SessionResponse,
response_model_by_alias=True,
dependencies=[Depends(current_admin_account)],
)
async def link_session(session: SessionDep, app: Annotated[str, Body(embed=True)]) -> VaultSecret:
"""Link a number for the Bot of an app."""
Expand Down Expand Up @@ -63,7 +67,10 @@ async def get_qr(session: SessionDep, session_id: str) -> dict[str, str]:


@router.post(
"/sessions/{session_id}/relink", response_model=SessionResponse, response_model_by_alias=True
"/sessions/{session_id}/relink",
response_model=SessionResponse,
response_model_by_alias=True,
dependencies=[Depends(current_admin_account)],
)
async def relink_session(session: SessionDep, session_id: str) -> VaultSecret:
"""Take a new scan on a number that lost its link."""
Expand All @@ -74,7 +81,9 @@ async def relink_session(session: SessionDep, session_id: str) -> VaultSecret:
raise HTTPException(404, "Session not found.")


@router.delete("/sessions/{session_id}", status_code=204)
@router.delete(
"/sessions/{session_id}", status_code=204, dependencies=[Depends(current_admin_account)]
)
async def remove_session(session: SessionDep, session_id: str) -> None:
connection = await Waha.get_session(session, session_id)
if not connection:
Expand Down
4 changes: 2 additions & 2 deletions backend/druks/core/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse

from druks.accounts.dependencies import current_session_account
from druks.accounts.dependencies import current_admin_account
from druks.apps.registry import services
from druks.core.apis.github import GITHUB_API_URL
from druks.core.apis.slack import SLACK_CREATE_APP_URL
Expand Down Expand Up @@ -50,7 +50,7 @@ async def create_github_app(slug: str) -> HTMLResponse:
@router.get(
"/{slug}/manifest/callback",
response_class=HTMLResponse,
dependencies=[Depends(current_session_account)],
dependencies=[Depends(current_admin_account)],
)
async def github_manifest_callback(slug: str, code: str = "") -> HTMLResponse:
service = get_manifest_service(slug)
Expand Down
12 changes: 7 additions & 5 deletions backend/druks/harnesses/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
from fastapi import APIRouter, Body, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession

from druks.accounts.dependencies import current_session_account, current_session_or_setup
from druks.accounts.dependencies import (
current_admin_account,
current_session_account,
current_session_or_setup,
)
from druks.accounts.models import Account
from druks.accounts.schemas import AccountResponse
from druks.api.dependencies import SessionDep
Expand Down Expand Up @@ -232,7 +236,7 @@ async def _store_connection(
async def create_key(
provider_id: str,
session: SessionDep,
account: Account = Depends(current_session_account),
account: Account = Depends(current_admin_account),
key: str = Body(..., embed=True),
) -> VaultSecret:
"""The installation's key at a provider. A key for a directory provider also adds it."""
Expand All @@ -254,9 +258,7 @@ async def create_key(
return await VaultSecret.paste(session, Audience.provider(provider_id), key, pasted_by=account)


@router.delete(
"/{provider_id}/key", status_code=204, dependencies=[Depends(current_session_account)]
)
@router.delete("/{provider_id}/key", status_code=204, dependencies=[Depends(current_admin_account)])
async def remove_key(session: SessionDep, provider_id: str) -> None:
"""Removing a directory provider's key also removes the provider."""
if stored := await VaultSecret.lookup(
Expand Down
Loading
Loading