Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 16 additions & 2 deletions carbonserver/carbonserver/api/routers/authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import logging
from typing import Optional
from urllib.parse import urlparse

from authlib.integrations.starlette_client import OAuthError
from dependency_injector.wiring import Provide, inject
Expand All @@ -27,6 +28,19 @@
router = APIRouter()


def get_redirect_url(request: Request) -> str:
base_url = settings.frontend_url or "http://localhost:3000"
redirect = request.query_params.get("redirect")
if redirect:
parsed = urlparse(redirect)
if parsed.scheme in ("http", "https") and parsed.netloc in (
"localhost:3000",
"127.0.0.1:3000",
):
return redirect
return f"{base_url.rstrip('/')}/home"


@router.get("/auth/check", name="auth-check")
@inject
def check_login(
Expand Down Expand Up @@ -81,7 +95,7 @@ async def get_login(
login and redirect to frontend app with token
"""
if auth_provider is None:
raise HTTPException(status_code=501, detail="Authentication not configured")
return RedirectResponse(get_redirect_url(request))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

CAN it be computed at startup of server?
Redirect url shouldn't change during app lifetime

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! We don't even need to read the redirect from the request, changing it here da71fbd

login_url = request.url_for("login")
if code:
try:
Expand Down Expand Up @@ -133,7 +147,7 @@ async def logout(
Logout user by clearing session and removing cookie
"""
if auth_provider is None:
raise HTTPException(status_code=501, detail="Authentication not configured")
return RedirectResponse(get_redirect_url(request))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same


# Revoke the access token at the OIDC provider before clearing it locally
access_token = request.cookies.get(SESSION_COOKIE_NAME)
Expand Down
15 changes: 15 additions & 0 deletions carbonserver/carbonserver/api/services/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from carbonserver.api.services.auth_providers.oidc_auth_provider import (
OIDCAuthProvider,
)
from carbonserver.api.services.signup_service import SignUpService
from carbonserver.api.services.user_service import UserService
from carbonserver.config import settings
from carbonserver.container import ServerContainer
Expand All @@ -25,6 +26,11 @@ class FullUser:


SESSION_COOKIE_NAME = "user_session"
LOCAL_DEV_AUTH_USER = {
"sub": "d1b9d5e0-58e8-45f0-9ef5-4549b3d6f3f0",
"email": "local.user@example.com",
"fields": {"name": "Local user"},
}


web_scheme = APIKeyCookie(name=SESSION_COOKIE_NAME, auto_error=False)
Expand All @@ -49,11 +55,20 @@ async def __call__(
user_service: Optional[UserService] = Depends(
Provide[ServerContainer.user_service]
),
sign_up_service: Optional[SignUpService] = Depends(
Provide[ServerContainer.sign_up_service]
),
auth_provider: Optional[OIDCAuthProvider] = Depends(
Provide[ServerContainer.auth_provider]
),
):
self.user_service = user_service
if settings.auth_provider.lower() == "none":
self.auth_user = LOCAL_DEV_AUTH_USER
sign_up_service.check_jwt_user(self.auth_user, create=True)
self.db_user = user_service.get_user_by_id(self.auth_user["sub"])
return self

if cookie_token is not None:
self.auth_user = jwt.decode(
cookie_token,
Expand Down
12 changes: 6 additions & 6 deletions carbonserver/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Dockerfile

# Use Ubuntu to install Python and uv
# For production, you could use python:3.11-slim
# For production, you could use python:3.12-slim

FROM ubuntu:22.04@sha256:3c61d3759c2639d4b836d32a2d3c83fa0214e36f195a3421018dbaaf79cbe37f

Expand All @@ -17,10 +17,10 @@ RUN apt-get update && apt-get upgrade -y && \
apt-get install -y software-properties-common curl && \
add-apt-repository ppa:deadsnakes/ppa -y && \
apt-get update && \
apt-get install -y gcc libpq-dev python3.11 python3.11-dev
apt-get install -y gcc libpq-dev python3.12 python3.12-dev

RUN ln -sf /usr/bin/python3.11 /usr/bin/python && \
ln -sf /usr/bin/python3.11 /usr/bin/python3
RUN ln -sf /usr/bin/python3.12 /usr/bin/python && \
ln -sf /usr/bin/python3.12 /usr/bin/python3

# Download the latest UV installer
ADD https://astral.sh/uv/install.sh /uv-installer.sh
Expand All @@ -36,8 +36,8 @@ COPY pyproject.toml /app/
COPY codecarbon /app/codecarbon
COPY carbonserver /app/carbonserver

# Install dependencies using uv with the api dependency group
RUN uv pip install --system -e ".[api]"
# Install carbonserver dependencies.
RUN uv pip install --system -e ./carbonserver

COPY ./carbonserver/docker/entrypoint.sh /opt
RUN chmod a+x /opt/entrypoint.sh
Expand Down
69 changes: 69 additions & 0 deletions carbonserver/tests/api/service/test_auth_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from unittest import mock

import pytest
from starlette.requests import Request

from carbonserver.api.routers import authenticate
from carbonserver.api.services import auth_service


@pytest.mark.asyncio
async def test_no_auth_provider_uses_local_dev_user(monkeypatch):
monkeypatch.setattr(auth_service.settings, "auth_provider", "none")
user_service = mock.Mock()
sign_up_service = mock.Mock()

dependency = auth_service.UserWithAuthDependency(error_if_not_found=True)
result = await dependency(
user_service=user_service,
sign_up_service=sign_up_service,
auth_provider=None,
)

assert result.auth_user == auth_service.LOCAL_DEV_AUTH_USER
sign_up_service.check_jwt_user.assert_called_once_with(
auth_service.LOCAL_DEV_AUTH_USER, create=True
)
user_service.get_user_by_id.assert_called_once_with(
auth_service.LOCAL_DEV_AUTH_USER["sub"]
)


@pytest.mark.asyncio
async def test_no_auth_login_redirects_to_frontend(monkeypatch):
monkeypatch.setattr(authenticate.settings, "frontend_url", "http://localhost:3000")
request = Request(
{
"type": "http",
"method": "GET",
"path": "/auth/login",
"headers": [],
"scheme": "http",
"server": ("localhost", 8008),
"client": ("testclient", 50000),
"query_string": b"redirect=http%3A%2F%2Flocalhost%3A3000%2Fhome%3Fauth%3Dtrue",
}
)

response = await authenticate.get_login(request, auth_provider=None)

assert response.status_code == 307
assert response.headers["location"] == "http://localhost:3000/home?auth=true"


def test_no_auth_login_rejects_external_redirect(monkeypatch):
monkeypatch.setattr(authenticate.settings, "frontend_url", "http://localhost:3000")
request = Request(
{
"type": "http",
"method": "GET",
"path": "/auth/login",
"headers": [],
"scheme": "http",
"server": ("localhost", 8008),
"client": ("testclient", 50000),
"query_string": b"redirect=https%3A%2F%2Fexample.com%2Fhome",
}
)

assert authenticate.get_redirect_url(request) == "http://localhost:3000/home"
146 changes: 32 additions & 114 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,133 +1,51 @@
# cp .env.example .env
# docker compose up -d
services:
###############################################
# Codecarbon-related services
###############################################
postgres:
image: postgres:13
environment:
POSTGRES_DB: codecarbon_db
POSTGRES_USER: codecarbon-user
POSTGRES_PASSWORD: supersecret
ports:
- "5432:5432"
volumes:
- postgres_codecarbon_data:/var/lib/postgresql/data

carbonserver:
depends_on:
- postgres
build:
context: .
dockerfile: ./carbonserver/docker/Dockerfile
depends_on:
- postgres
volumes:
- ./carbonserver:/carbonserver
labels:
- "traefik.enable=true"
# - >
# traefik.http.routers.carbonserver.rule=(
# Host(`${APP_HOSTNAME}`) && (
# PathPrefix(`/users`) || PathPrefix(`/auth`) || PathPrefix(`/docs`) ||
# PathPrefix(`/organizations`) || PathPrefix(`/runs`) || PathPrefix(`/emissions`) ||
# PathPrefix(`/projects`)|| PathPrefix(`/api`) || PathPrefix(`/auth-callback`)
# ))"
- "traefik.http.routers.carbonserver.rule=(Host(`${APP_HOSTNAME}`) && (PathPrefix(`/users`) || PathPrefix(`/auth`)|| PathPrefix(`/docs`)|| PathPrefix(`/organizations`) || PathPrefix(`/runs`) || PathPrefix(`/emissions`) || PathPrefix(`/projects`)|| PathPrefix(`/api`) || PathPrefix(`/auth-callback`) ))"
- "traefik.http.routers.carbonserver.entrypoints=web,websecure"
# - "traefik.http.routers.carbonserver.tls.certresolver=myresolver"
# - "traefik.http.routers.carbonserver.tls={}"
- "traefik.http.routers.carbonserver.priority=10000"
- "traefik.http.services.carbonserver.loadbalancer.server.port=8000"
- "traefik.docker.network=shared"
# ports:
# - "8000:8000"
env_file:
- ./.env
environment:
CODECARBON_LOG_LEVEL: DEBUG
DATABASE_URL: postgresql://${DATABASE_USER:-codecarbon-user}:${DATABASE_PASS:-supersecret}@${DATABASE_HOST:-postgres}:${DATABASE_PORT:-5432}/${DATABASE_NAME:-codecarbon_db}
networks:
- default
- shared
AUTH_PROVIDER: ${AUTH_PROVIDER:-none}
ENVIRONMENT: ${ENVIRONMENT:-local}
DATABASE_URL: postgresql://codecarbon-user:supersecret@postgres:5432/codecarbon_db
FRONTEND_URL: http://localhost:3000
CORS_ORIGINS: http://localhost:3000
ports:
- "8008:8000"

ui:
build:
context: ./webapp
dockerfile: Dockerfile

# Set environment variables based on the .env file
env_file:
- ./webapp/.env.development
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.ui.rule=Host(`${APP_HOSTNAME}`)"
- "traefik.http.routers.ui.entrypoints=web,websecure"
# - "traefik.http.routers.ui.tls.certresolver=myresolver"
- "traefik.http.routers.ui.priority=1"
- "traefik.http.services.ui.loadbalancer.server.port=3000"
- "traefik.docker.network=shared"

# ports:
# - "3000:3000"
networks:
- default
- shared

postgres:
# container_name: ${DATABASE_HOST:-postgres_codecarbon}
depends_on:
- carbonserver
environment:
HOSTNAME: ${DATABASE_HOST:-postgres_codecarbon}
POSTGRES_DB: ${DATABASE_NAME:-codecarbon_db}
POSTGRES_PASSWORD: ${DATABASE_PASS:-supersecret}
POSTGRES_USER: ${DATABASE_USER:-codecarbon-user}
image: postgres:13
# ports:
# - 5480:5432
restart: unless-stopped
VITE_API_URL: http://localhost:8008
VITE_BASE_URL: http://localhost:3000
VITE_USE_MOCK_DATA: "false"
VITE_PROJECT_ENCRYPTION_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
volumes:
- postgres_codecarbon_data:/var/lib/postgresql/data:rw
networks:
- default

# pgadmin:
# # container_name: pgadmin_codecarbon
# image: dpage/pgadmin4
# environment:
# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-test@test.com}
# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-test}
# volumes:
# - pgadmin:/root/.pgadmin
# - ./carbonserver/docker/pgpassfile:/pgadmin4/pgpassfile
# - ./carbonserver/docker/pgadmin-servers.json:/pgadmin4/servers.json
# # ports:
# # - "${PGADMIN_PORT:-5080}:80"
# networks:
# - default
# restart: unless-stopped

###############################################
# Prometheus-related services
###############################################
# Uncomment the following to enable prometheus and pushgateway

# prometheus:
# image: prom/prometheus:latest
# ports:
# - "9090:9090"
# volumes:
# - ./docker/prometheus.yml:/etc/prometheus/prometheus.yml
# depends_on:
# - "prometheus-pushgateway"
# networks:
# - default
# - shared

# prometheus-pushgateway:
# image: prom/pushgateway
# ports:
# - "9091:9091"
# networks:
# - default
# - shared
- ./webapp:/app
- webapp_node_modules:/app/node_modules
command: sh -c "corepack enable pnpm && pnpm dev --host 0.0.0.0 --port 5173"
ports:
- "3000:5173"

volumes:
postgres_codecarbon_data:
name: postgres_codecarbon_data1
pgadmin:
name: pgadmin_codecarbon_data1

networks:
default:
driver: bridge
shared: # traefik network
external: true
webapp_node_modules:
Loading