Skip to content

Commit

Permalink
Switch to OAuth2
Browse files Browse the repository at this point in the history
  • Loading branch information
frodrigo committed Feb 27, 2024
1 parent 11bd2bb commit 4b6003a
Show file tree
Hide file tree
Showing 11 changed files with 81 additions and 119 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.mypy_cache
web/node_modules
web/public/assets
docker/.env
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ osmose-frontend-venv
web/public/assets

.mypy_cache

docker/.env
3 changes: 3 additions & 0 deletions docker/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OSM_CLIENT_ID=
OSM_CLIENT_SECRET=
COOKIE_SIGN_KEY=
2 changes: 2 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Docker

Copy `.env.template` as `.env` and adjustcontent, only required to enable loggin from osm.org.

Build the Docker image, within the docker directory:
```
curl http://osmose.openstreetmap.fr/export/osmose-menu.sql.bz2 | bzcat > osmose-menu.sql
Expand Down
3 changes: 3 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ services:
- postgres
environment:
- DB_HOST=postgres
- OSM_CLIENT_ID=${OSM_CLIENT_ID}
- OSM_CLIENT_SECRET=${OSM_CLIENT_SECRET}
- COOKIE_SIGN_KEY=${COOKIE_SIGN_KEY}
ports:
- 20009:20009
networks:
Expand Down
6 changes: 2 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ pyclipper
fastapi
fastapi-sessions
python-multipart
# Use a patch fork, for unmaintened rauth:
# - https://github.com/litl/rauth/issues/185 (data dict/bytes issue)
# - https://github.com/litl/rauth/pull/208 (python 3.8 compatibility)
git+https://github.com/osm-fr/rauth.git
Authlib
httpx
lxml
56 changes: 42 additions & 14 deletions web_api/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import os
from typing import Any, Dict, Optional
from uuid import UUID, uuid4

from fastapi import Depends, FastAPI, Response
import requests
from authlib.integrations.starlette_client import OAuth # type: ignore
from fastapi import Depends, FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi.responses import HTMLResponse, RedirectResponse
from starlette.config import Config
from starlette.middleware.sessions import SessionMiddleware

from modules import utils
from modules.dependencies import database
from modules.dependencies import database, langs
from modules.utils import LangsNegociation

from . import byuser, editor, false_positive, issue, issues, map
from .tool import oauth
from .tool.session import SessionData, backend, cookie, verifier

openapi_tags = [
Expand All @@ -37,17 +42,37 @@ def custom_openapi() -> Dict[str, Any]:

app.openapi = custom_openapi # type: ignore

app.add_middleware(SessionMiddleware, secret_key=os.getenv("COOKIE_SIGN_KEY", ""))

oauth = OAuth(
Config(
environ={
"OSM_CLIENT_ID": os.getenv("OSM_CLIENT_ID", ""),
"OSM_CLIENT_SECRET": os.getenv("OSM_CLIENT_SECRET", ""),
}
)
)
oauth.register(
name="osm",
server_metadata_url="https://www.openstreetmap.org/.well-known/oauth-authorization-server",
client_kwargs={"scope": "read_prefs write_api write_notes"},
)


@app.get("/login")
async def login(session_id: Optional[UUID] = Depends(cookie)) -> RedirectResponse:
async def login(
request: Request,
session_id: Optional[UUID] = Depends(cookie),
langs: LangsNegociation = Depends(langs.langs),
) -> RedirectResponse:
if session_id:
await backend.delete(session_id)

(url, oauth_tokens) = oauth.fetch_request_token()
session = uuid4()
await backend.create(session, SessionData(oauth_tokens=oauth_tokens))
await backend.create(session, SessionData())

response = RedirectResponse(url)
redirect_uri = utils.website + "/en/oauth2"
response = await oauth.osm.authorize_redirect(request, redirect_uri)
cookie.attach_to_response(response, session)
return response

Expand All @@ -62,19 +87,22 @@ async def logout(
return RedirectResponse("map/")


@app.get("/oauth")
async def oauth_(
@app.get("/oauth2")
async def oauth2(
request: Request,
session_id: UUID = Depends(cookie),
session_data: Optional[SessionData] = Depends(verifier),
) -> RedirectResponse:
if session_id and session_data:
try:
oauth_tokens = oauth.fetch_access_token(session_data.oauth_tokens)
session_data.oauth_tokens = oauth_tokens
user_request = oauth.get(
oauth_tokens, utils.remote_url + "api/0.6/user/details.json"
oauth2_token = await oauth.osm.authorize_access_token(request)
session_data.oauth2_token = oauth2_token["access_token"]

user_request = requests.get(
utils.remote_url + "api/0.6/user/details.json",
headers={"Authorization": f"Bearer {session_data.oauth2_token}"},
)
if user_request:
if user_request and user_request.status_code == 200:
session_data.user = user_request.json()
await backend.update(session_id, session_data)
except Exception:
Expand Down
48 changes: 23 additions & 25 deletions web_api/editor.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import io
from typing import Dict, Optional, Tuple
from typing import Dict, Optional
from uuid import UUID

import requests
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, Request

from modules import OsmSax, utils
from modules.dependencies import database

from .tool import oauth
from .tool.session import SessionData, backend, cookie, verifier

router = APIRouter()
Expand All @@ -21,7 +21,7 @@ async def save(
session_id: UUID = Depends(cookie),
session_data: Optional[SessionData] = Depends(verifier),
) -> None:
if not session_data:
if not session_data or not session_data.oauth2_token:
raise HTTPException(status_code=401)

json = await request.json()
Expand All @@ -44,22 +44,22 @@ async def save(
changeset = session_data.changeset
if changeset and not reuse_changeset:
try:
_changeset_close(session_data.oauth_tokens, changeset)
_changeset_close(session_data.oauth2_token, changeset)
except Exception:
pass
changeset = None
session_data.changeset = None
await backend.update(session_id, session_data)
elif changeset:
try:
_changeset_update(session_data.oauth_tokens, changeset, tags)
_changeset_update(session_data.oauth2_token, changeset, tags)
except Exception:
changeset = None
session_data.changeset = changeset
await backend.update(session_id, session_data)

if not changeset:
changeset = _changeset_create(session_data.oauth_tokens, tags)
changeset = _changeset_create(session_data.oauth2_token, tags)
session_data.changeset = changeset
await backend.update(session_id, session_data)

Expand Down Expand Up @@ -91,7 +91,7 @@ async def save(
osmchange = out.getvalue()

# Fire the changeset
_changeset_upload(session_data.oauth_tokens, changeset, osmchange)
_changeset_upload(session_data.oauth2_token, changeset, osmchange)


def _osm_changeset(tags, id: str = "0") -> str:
Expand All @@ -108,35 +108,33 @@ def _osm_changeset(tags, id: str = "0") -> str:
return out.getvalue()


def _changeset_create(oauth_tokens: Tuple[str, str], tags: Dict[str, str]) -> str:
changeset = oauth.put(
oauth_tokens,
def _changeset_create(oauth2_token: str, tags: Dict[str, str]) -> str:
request = requests.put(
utils.remote_url_write + "api/0.6/changeset/create",
_osm_changeset(tags),
data=_osm_changeset(tags),
headers={"Authorization": f"Bearer {oauth2_token}"},
)
return changeset
return request.text


def _changeset_update(
oauth_tokens: Tuple[str, str], id: str, tags: Dict[str, str]
) -> None:
oauth.put(
oauth_tokens,
def _changeset_update(oauth2_token: str, id: str, tags: Dict[str, str]) -> None:
requests.put(
utils.remote_url_write + "api/0.6/changeset/" + id,
_osm_changeset(tags, id=id),
data=_osm_changeset(tags, id=id),
headers={"Authorization": f"Bearer {oauth2_token}"},
)


def _changeset_close(oauth_tokens: Tuple[str, str], id: str) -> None:
oauth.put(
oauth_tokens,
def _changeset_close(oauth2_token: str, id: str) -> None:
requests.put(
utils.remote_url_write + "api/0.6/changeset/" + id + "/close",
headers={"Authorization": f"Bearer {oauth2_token}"},
)


def _changeset_upload(oauth_tokens: Tuple[str, str], id: str, osmchange) -> None:
oauth.post(
oauth_tokens,
def _changeset_upload(oauth2_token: str, id: str, osmchange) -> None:
requests.post(
utils.remote_url_write + "api/0.6/changeset/" + id + "/upload",
osmchange,
data=osmchange,
headers={"Authorization": f"Bearer {oauth2_token}"},
)
2 changes: 1 addition & 1 deletion web_api/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async def index(
timestamp = await db.fetchval(sql)

if session_data and session_data.user:
user = session_data.user["osm"]["user"]["@display_name"]
user = session_data.user["user"]["display_name"]
user_error_count = await _user_count(params, db, user)
else:
user = None
Expand Down
73 changes: 0 additions & 73 deletions web_api/tool/oauth.py

This file was deleted.

4 changes: 2 additions & 2 deletions web_api/tool/session.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import pathlib
from typing import Any, Dict, Generic, Optional, Tuple, TypeVar, Union
from typing import Any, Dict, Generic, Optional, TypeVar, Union
from uuid import UUID

from fastapi import HTTPException, Request
Expand All @@ -19,7 +19,7 @@


class SessionData(BaseModel):
oauth_tokens: Tuple[str, str]
oauth2_token: Optional[str] = None
user: Optional[Dict[str, Any]] = None
changeset: Optional[Any] = None

Expand Down

0 comments on commit 4b6003a

Please sign in to comment.