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

OIDC/Auth2 integration #134

Open
wants to merge 6 commits into
base: feature/keycloak-oidc
Choose a base branch
from
Open
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
17 changes: 12 additions & 5 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
FROM python:3.10.15-slim-bookworm
FROM python:3.10.15-slim-bookworm as base

RUN apt-get -y update && apt-get install --no-install-recommends -y \
postgresql-client libpq-dev gcc libc-dev git

WORKDIR /app

COPY requirements_production.txt .
COPY requirements_development.txt .
RUN pip install -r requirements_development.txt

COPY scripts/entrypoint.sh .
COPY scripts/execute-cleanup.sh .
COPY setup.cfg .
Expand All @@ -20,3 +16,14 @@ EXPOSE 9006

ENTRYPOINT ["./entrypoint.sh"]
CMD exec flask --app src/mediaserver run --host 0.0.0.0 --port 9006 --debug

FROM base as development

COPY requirements_*.txt .
RUN pip install -r requirements_development.txt

FROM base as development-fullstack

COPY requirements_*.txt .
COPY --from=pipauth / /pip-auth
RUN pip install -r requirements_development_fullstack.txt
3 changes: 3 additions & 0 deletions Dockerfile.tests
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ RUN apt-get -y update && apt-get -y upgrade && \
WORKDIR /app

COPY requirements*.txt ./
# The pip-auth library is copied from the openslides-auth-service in the Makefile before the build
# ... in docker-compose.dev.yml the pip-auth is mounted to the container
COPY pip-auth /pip-auth
RUN pip install -r requirements_tests.txt

COPY setup.cfg .
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Copy link
Member

Choose a reason for hiding this comment

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

The auth service should be added as submodule to this repo instead.

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
build-dev:
docker build . -f Dockerfile.dev --tag openslides-media-dev

build-dev-fullstack:
DOCKER_BUILDKIT=1 docker build . -f Dockerfile.dev --target development-fullstack --build-context pipauth=../openslides-auth-service/libraries/pip-auth --tag openslides-media-dev-fullstack

build-tests:
docker build . -f Dockerfile.tests --tag openslides-media-tests

Expand Down
6 changes: 6 additions & 0 deletions requirements_common.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Flask==3.0.3
gunicorn==23.0.0
psycopg2==2.9.9
requests==2.32.3
authlib==1.3.1
pyjwt==2.9.0
8 changes: 8 additions & 0 deletions requirements_development_fullstack.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-r requirements_common.txt

autoflake==2.3.1
black==24.8.0
flake8==7.1.1
isort==5.13.2

-e /pip-auth
7 changes: 2 additions & 5 deletions requirements_production.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
Flask==3.0.3
gunicorn==23.0.0
psycopg2==2.9.9
requests==2.32.3
-r requirements_common.txt

# authlib
git+https://github.com/OpenSlides/openslides-auth-service.git@6548f50ca778ab4f27e530886080f1482681929d#egg=authlib&subdirectory=auth/libraries/pip-auth
git+https://github.com/kryptance/openslides-auth-service.git@01940817cf3435a7c20b4bcca076317f15f7fe23#egg=authlib&subdirectory=libraries/pip-auth
Empty file added src/auth/__init__.py
Empty file.
49 changes: 25 additions & 24 deletions src/auth.py → src/auth/auth.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
from urllib import parse

import requests
from authlib import (
AUTHENTICATION_HEADER,
COOKIE_NAME,
AuthenticateException,
AuthHandler,
InvalidCredentialsException,
)
from flask import current_app as app
from flask import request

from .exceptions import ServerError
from os_authlib import (
AUTHENTICATION_HEADER,
AuthenticateException,
AuthHandler,
InvalidCredentialsException, AUTHORIZATION_HEADER, )
from ..exceptions import ServerError


def check_login_valid():
"""Returns whether the user is logged in or not."""
def get_user_id():
"""Returns the user id from the auth cookie."""
auth_handler = AuthHandler(app.logger.debug)
cookie = request.cookies.get(COOKIE_NAME, "")
authentication = request.headers.get(AUTHORIZATION_HEADER, "")
app.logger.info(f"Get user id from auth header: {authentication}")
try:
auth_handler.authenticate_only_refresh_id(parse.unquote(cookie))
(user_id, _) = auth_handler.authenticate(authentication)
except (AuthenticateException, InvalidCredentialsException):
return -1
return user_id


def check_login():
"""Returns whether the user is logged in or not."""
user_id = get_user_id()
if user_id == -1:
return False
return True


def check_file_id(file_id, autoupdate_headers):
def check_file_id(file_id, autoupdate_headers, user_id):
"""
Returns a triple: ok, filename, auth_header.
filename is given, if ok=True. If ok=false, the user has no perms.
if auth_header is returned, it must be set in the response.
"""
auth_handler = AuthHandler(app.logger.debug)
cookie = request.cookies.get(COOKIE_NAME, "")
try:
user_id = auth_handler.authenticate_only_refresh_id(parse.unquote(cookie))
except (AuthenticateException, InvalidCredentialsException):
raise ServerError("Could not parse auth cookie")
if user_id == -1:
raise ServerError("Could not find authentication")

autoupdate_url = get_autoupdate_url(user_id)
payload = [
Expand Down Expand Up @@ -71,11 +72,11 @@ def check_file_id(file_id, autoupdate_headers):
if not isinstance(content, dict):
raise ServerError("The returned content is not a dict.")

auth_header = response.headers.get(AUTHENTICATION_HEADER)
auth_header = response.headers.get(AUTHORIZATION_HEADER)

if (
f"mediafile/{file_id}/id" not in content
or content[f"mediafile/{file_id}/id"] != file_id
f"mediafile/{file_id}/id" not in content
or content[f"mediafile/{file_id}/id"] != file_id
):
return False, None, auth_header

Expand Down
55 changes: 40 additions & 15 deletions src/mediaserver.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import atexit
import base64
import json
import os
import sys
from signal import SIGINT, SIGTERM, signal

from authlib.integrations.flask_oauth2 import ResourceProtector, current_token
from authlib.oauth2 import OAuth2Error
from flask import Flask, Response, jsonify, redirect, request
from flask import json

from .auth import AUTHENTICATION_HEADER, check_file_id, check_login_valid
from os_authlib.token_validator import create_openslides_token_validator
from .auth.auth import AUTHENTICATION_HEADER, check_file_id
from .config_handling import init_config, is_dev_mode
from .database import Database
from .exceptions import BadRequestError, HttpError, NotFoundError
Expand All @@ -18,32 +23,50 @@
init_config()
database = Database()

app.logger.info("Started media server")
app.config['DEBUG'] = True
app.debug = True

require_oauth = ResourceProtector()
require_oauth.register_token_validator(create_openslides_token_validator())

@app.errorhandler(HttpError)
@app.errorhandler(Exception)
def handle_view_error(error):
app.logger.error(
f"Request to {request.path} resulted in {error.status_code}: "
f"{error.message}"
)
res_content = {"message": f"Media-Server: {error.message}"}
response = jsonify(res_content)
response.status_code = error.status_code
return response
raise error
# if isinstance(error, HttpError):
# app.logger.error(
# f"Request to {request.path} resulted in {error.status_code}: "
# f"{error.message}"
# )
# res_content = {"message": f"Media-Server: {error.message}"}
# response = jsonify(res_content)
# response.status_code = error.status_code
# return response
# elif isinstance(error, OAuth2Error):
# app.logger.error(
# f"Request to {request.path} resulted in {error.status_code}: "
# f"{error.description} (AuthlibHTTPError)"
# )
# res_content = {"message": f"Media-Server: {error.description}"}
# response = jsonify(res_content)
# response.status_code = error.status_code
# return response
# else:
# app.logger.error(f"Request to {request.path} resulted in {error} ({type(error)})")
# res_content = {"message": "Media-Server: Internal Server Error"}
# response = jsonify(res_content)
# response.status_code = 500
# return response


@app.route("/system/media/get/<int:file_id>")
@require_oauth()
def serve(file_id):
if not check_login_valid():
return redirect("/")

# get file id
autoupdate_headers = dict(request.headers)
del_keys = [key for key in autoupdate_headers if "content" in key]
for key in del_keys:
del autoupdate_headers[key]
ok, filename, auth_header = check_file_id(file_id, autoupdate_headers)
ok, filename, auth_header = check_file_id(file_id, autoupdate_headers, current_token.os_uid)
if not ok:
raise NotFoundError()

Expand Down Expand Up @@ -76,6 +99,7 @@ def chunked(size, source):


@app.route("/internal/media/upload_mediafile/", methods=["POST"])
@require_oauth()
def media_post():
dejson = get_json_from_request()
try:
Expand All @@ -96,6 +120,7 @@ def media_post():


@app.route("/internal/media/duplicate_mediafile/", methods=["POST"])
@require_oauth()
def duplicate_mediafile():
source_id, target_id = get_ids(get_json_from_request())
app.logger.debug(f"source_id {source_id} and target_id {target_id}")
Expand Down
10 changes: 5 additions & 5 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from collections.abc import Mapping
from os.path import join

import jwt
import psycopg2
import pytest
import requests
from authlib import COOKIE_NAME
from authlib.config import AUTH_DEV_COOKIE_SECRET
from os_authlib import COOKIE_NAME
from os_authlib.config import AUTH_DEV_COOKIE_SECRET

GET_URL = "http://media:9006/system/media/get/"

Expand Down Expand Up @@ -34,6 +35,5 @@ def get_mediafile(id, use_cookie=True):
if use_cookie:
# dummy cookie for testing
token = jwt.encode({"userId": 1}, AUTH_DEV_COOKIE_SECRET)
cookie = f"bearer {token}"
cookies[COOKIE_NAME] = cookie
return requests.get(join(GET_URL, str(id)), cookies=cookies, allow_redirects=False)
authentication = f"bearer {token}"
return requests.get(join(GET_URL, str(id)), headers={'Authentication': authentication}, allow_redirects=False)
Loading