Skip to content

Commit

Permalink
feat: add google authentication proxy middleware plugin (#683)
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfv authored Jan 30, 2024
1 parent eea3eef commit e7b3940
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 10 deletions.
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dependencies:
- adlfs
- importlib_metadata
- pre-commit
- pytest
- pytest 7.*
- pytest-mock
- rq
- libcflib
Expand Down
Empty file.
123 changes: 123 additions & 0 deletions plugins/quetz_googleiap/quetz_googleiap/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import logging
import uuid

from starlette.middleware.base import BaseHTTPMiddleware

import quetz.authentication.base as auth_base
from quetz import rest_models
from quetz.config import Config, ConfigEntry, ConfigSection
from quetz.dao import Dao
from quetz.deps import get_config, get_db

logger = logging.getLogger("quetz.googleiam")


def email_to_channel_name(email):
name = email.split("@")[0]
name = name.replace(".", "-")
name = name.replace("_", "-")
return name


class GoogleIAMMiddleware(BaseHTTPMiddleware):
"""
Handles Google IAM headers and authorizes users based on the
Google IAM headers.
"""

def __init__(self, app, config: Config):
if config is not None:
self.configure(config)
else:
self.configured = False

super().__init__(app)

def configure(self, config: Config):
config.register(
[
ConfigSection(
"googleiam",
[
ConfigEntry("server_admin_emails", list, default=[]),
],
)
]
)

# load configuration values
if config.configured_section("googleiam"):
self.server_admin_emails = config.googleiam_server_admin_emails
logger.info("Google IAM successfully configured")
logger.info(f"Google IAM server admin emails: {self.server_admin_emails}")
self.configured = True
else:
self.configured = False

async def dispatch(self, request, call_next):
# ignore middleware if it is not configured
if not self.configured or request.url.path.startswith("/health"):
response = await call_next(request)
return response

user_id = request.headers.get("x-goog-authenticated-user-id")
email = request.headers.get("x-goog-authenticated-user-email")

if user_id and email:
db = next(get_db(get_config()))
dao = Dao(db)

_, email = email.split(":", 1)
_, user_id = user_id.split(":", 1)

user = dao.get_user_by_username(email)
if not user:
email_data: auth_base.Email = {
"email": email,
"verified": True,
"primary": True,
}
user = dao.create_user_with_profile(
email, "google", user_id, email, "", None, True, [email_data]
)
user_channel = email_to_channel_name(email)

if dao.get_channel(email_to_channel_name(user_channel)) is None:
logger.info(f"Creating channel for user: {user_channel}")
channel = rest_models.Channel(
name=user_channel,
private=False,
description="Channel for user: " + email,
)
dao.create_channel(channel, user.id, "owner")

self.google_role_for_user(user_id, email, dao)
user_id = uuid.UUID(bytes=user.id)
# drop the db and dao to remove the connection
del db, dao
# we also need to find the role of the user
request.session['identity_provider'] = "dummy"
request.session["user_id"] = str(user_id)
else:
request.session["user_id"] = None
request.session["identity_provider"] = None

response = await call_next(request)
return response

def google_role_for_user(self, user_id, username, dao):
if not user_id or not username:
return

if username in self.server_admin_emails:
logger.info(f"User '{username}' with user id '{user_id}' is server admin")
dao.set_user_role(user_id, "owner")
else:
logger.info(
f"User '{username}' with user id '{user_id}' is not a server admin"
)
dao.set_user_role(user_id, "member")


def middleware():
return GoogleIAMMiddleware
14 changes: 14 additions & 0 deletions plugins/quetz_googleiap/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from setuptools import setup

plugin_name = "quetz-googleiap"

setup(
name=plugin_name,
install_requires=[],
entry_points={
"quetz.middlewares": [f"{plugin_name} = quetz_googleiap.middleware"],
},
packages=[
"quetz_googleiap",
],
)
15 changes: 15 additions & 0 deletions plugins/quetz_googleiap/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import pytest

pytest_plugins = "quetz.testing.fixtures"


@pytest.fixture
def plugins():
# defines plugins to enable for testing
return ['quetz-googleiap']


@pytest.fixture
def sqlite_in_memory():
# use sqlite on disk so that we can modify it in a different process
return False
23 changes: 23 additions & 0 deletions plugins/quetz_googleiap/tests/test_googleiap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest


@pytest.mark.parametrize(
"config_extra", ['[googleiam]\nserver_admin_emails=["[email protected]"]']
)
def test_authentication(client, db):
response = client.get("/api/me")
assert response.status_code == 401

# add headers
headers = {
'X-Goog-Authenticated-User-Email': 'accounts.google.com:[email protected]',
'X-Goog-Authenticated-User-Id': 'accounts.google.com:[email protected]',
}

response = client.get("/api/me", headers=headers)
assert response.status_code == 200

# # check if channel was created
# response = client.get("/api/channels", headers=headers)
# assert response.status_code == 200
# assert response.json()['channels'][0]['name'] == 'someone'
20 changes: 12 additions & 8 deletions quetz/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,6 @@

logger = logging.getLogger("quetz")

app.add_middleware(
SessionMiddleware,
secret_key=config.session_secret,
https_only=config.session_https_only,
)

if config.general_redirect_http_to_https:
logger.info("Configuring http to https redirect ")
app.add_middleware(HTTPSRedirectMiddleware)
Expand Down Expand Up @@ -163,6 +157,18 @@ async def dispatch(self, request, call_next):

app.add_middleware(CondaTokenMiddleware)

plugin_middlewares: List[Type[BaseHTTPMiddleware]] = [
ep.load() for ep in entry_points().select(group='quetz.middlewares')
]

for middleware in plugin_middlewares:
app.add_middleware(middleware.middleware(), config=config)

app.add_middleware(
SessionMiddleware,
secret_key=config.session_secret,
https_only=config.session_https_only,
)

if config.configured_section("profiling") and config.profiling_enable_sampling:
from pyinstrument.profiler import Profiler
Expand All @@ -187,8 +193,6 @@ async def profile_request(
pkgstore = config.get_package_store()

# authenticators


builtin_authenticators: List[Type[BaseAuthenticator]] = [
authenticator
for authenticator in [
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ dev =
flake8
isort
pre-commit
pytest
pytest >=7,<8
pytest-asyncio
pytest-mock
pytest-cov
Expand Down

0 comments on commit e7b3940

Please sign in to comment.