Skip to content

Commit c32edc8

Browse files
authored
Merge pull request #101 from Pantheon-temple/dev
Dev
2 parents 6dd7a35 + 787c9ef commit c32edc8

16 files changed

Lines changed: 222 additions & 11 deletions

File tree

.coveragerc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[run]
2+
omit =
3+
prometheus/script/*

.github/workflows/pytest_and_coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
# General settings
1818
PROMETHEUS_ENVIRONMENT: local
1919
PROMETHEUS_BACKEND_CORS_ORIGINS: "[\"*\"]"
20+
PROMETHEUS_ENABLE_AUTHENTICATION: false
2021

2122
# Neo4j settings
2223
PROMETHEUS_NEO4J_URI: bolt://localhost:7687

example.env

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ PROMETHEUS_LOGGING_LEVEL=DEBUG
33

44
# General settings
55
PROMETHEUS_ENVIRONMENT=local
6-
PROMETHEUS_BACKEND_CORS_ORIGINS=["*]
6+
PROMETHEUS_BACKEND_CORS_ORIGINS=["*"]
7+
PROMETHEUS_ENABLE_AUTHENTICATION=false
8+
79

810
# Neo4j settings
911
PROMETHEUS_NEO4J_URI=bolt://neo4j:7687

prometheus/app/api/issue.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastapi import APIRouter, Request
22

3+
from prometheus.app.decorators.require_login import requireLogin
34
from prometheus.app.models.requests.issue import IssueRequest
45
from prometheus.app.models.response.issue import IssueResponse
56
from prometheus.app.models.response.response import Response
@@ -16,6 +17,7 @@
1617
response_description="Returns the patch, test results, and issue response",
1718
response_model=Response[IssueResponse],
1819
)
20+
@requireLogin
1921
def answer_issue(issue: IssueRequest, request: Request) -> Response[IssueResponse]:
2022
if not request.app.state.service["knowledge_graph_service"].exists():
2123
raise ServerException(

prometheus/app/api/repository.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,61 @@
11
import git
22
from fastapi import APIRouter, Request
33

4+
from prometheus.app.decorators.require_login import requireLogin
45
from prometheus.app.models.response.response import Response
56
from prometheus.app.services.knowledge_graph_service import KnowledgeGraphService
67
from prometheus.app.services.repository_service import RepositoryService
8+
from prometheus.app.services.user_service import UserService
79
from prometheus.exceptions.server_exception import ServerException
810

911
router = APIRouter()
1012

1113

14+
def get_github_token(request: Request, github_token: str) -> str:
15+
"""Retrieve GitHub token from the request or user profile."""
16+
# If the token is provided in the request, use it directly
17+
if github_token:
18+
return github_token
19+
# If the token is not provided, fetch it from the user profile if logged in
20+
# Check if the user is authenticated
21+
if not request.state.user_id:
22+
# If the user is not authenticated, raise an exception
23+
raise ServerException(
24+
code=401, message="GitHub token is required, please provide it or log in"
25+
)
26+
# If the user is authenticated, get the user service and fetch the token
27+
user_service: UserService = request.app.state.service["user_service"]
28+
user = user_service.get_user_by_id(request.state.user_id)
29+
github_token = user.github_token if user else None
30+
31+
# If the token is still not available, raise an exception
32+
if not github_token:
33+
raise ServerException(
34+
code=423, message="Either provide a GitHub token or set it in your user profile"
35+
)
36+
return github_token
37+
38+
1239
@router.get(
1340
"/github/",
1441
description="""
1542
Upload a GitHub repository to Prometheus, default to the latest commit in the main branch.
1643
""",
1744
response_model=Response,
1845
)
46+
@requireLogin
1947
def upload_github_repository(github_token: str, https_url: str, request: Request):
2048
# Get the repository and knowledge graph services
2149
repository_service: RepositoryService = request.app.state.service["repository_service"]
2250
knowledge_graph_service: KnowledgeGraphService = request.app.state.service[
2351
"knowledge_graph_service"
2452
]
53+
github_token = get_github_token(request, github_token)
2554

2655
# Clean the services to ensure no previous data is present
2756
repository_service.clean()
2857
knowledge_graph_service.clear()
58+
2959
try:
3060
# Clone the repository
3161
saved_path = repository_service.clone_github_repo(github_token, https_url)
@@ -43,6 +73,7 @@ def upload_github_repository(github_token: str, https_url: str, request: Request
4373
""",
4474
response_model=Response,
4575
)
76+
@requireLogin
4677
def upload_github_repository_at_commit(
4778
github_token, https_url: str, commit_id: str, request: Request
4879
):
@@ -52,6 +83,8 @@ def upload_github_repository_at_commit(
5283
"knowledge_graph_service"
5384
]
5485

86+
github_token = get_github_token(request, github_token)
87+
5588
# Clean the services to ensure no previous data is present
5689
repository_service.clean()
5790
knowledge_graph_service.clear()
@@ -73,6 +106,7 @@ def upload_github_repository_at_commit(
73106
""",
74107
response_model=Response,
75108
)
109+
@requireLogin
76110
def delete(request: Request):
77111
knowledge_graph_service: KnowledgeGraphService = request.app.state.service[
78112
"knowledge_graph_service"
@@ -88,6 +122,7 @@ def delete(request: Request):
88122
return Response()
89123

90124

125+
@requireLogin
91126
@router.get(
92127
"/exists/",
93128
description="""

prometheus/app/decorators/__init__.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import inspect
2+
from functools import wraps
3+
4+
5+
def requireLogin(func):
6+
"""
7+
Decorator to indicate that a route requires user authentication.
8+
This decorator can be used to mark routes that should only be accessible to authenticated users.
9+
"""
10+
11+
@wraps(func)
12+
async def wrapper(*args, **kwargs):
13+
if inspect.iscoroutinefunction(func):
14+
return await func(*args, **kwargs)
15+
else:
16+
return func(*args, **kwargs)
17+
18+
# Set a custom attribute to indicate that this route requires login
19+
setattr(wrapper, "_require_login", True)
20+
return wrapper

prometheus/app/main.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@
33
from datetime import datetime, timezone
44

55
from fastapi import FastAPI
6+
from fastapi.middleware.cors import CORSMiddleware
67
from fastapi.routing import APIRoute
78

89
from prometheus.app import dependencies
9-
from prometheus.app.api import issue, repository
10+
from prometheus.app.api import auth, issue, repository
1011
from prometheus.app.exception_handler import register_exception_handlers
12+
from prometheus.app.middlewares.jwt_middleware import JWTMiddleware
13+
from prometheus.app.register_login_required_routes import (
14+
login_required_routes,
15+
register_login_required_routes,
16+
)
1117
from prometheus.configuration.config import settings
1218

1319
# Create a logger for the application's namespace
@@ -67,12 +73,33 @@ def custom_generate_unique_id(route: APIRoute) -> str:
6773
debug=True if settings.ENVIRONMENT == "local" else False,
6874
)
6975

70-
# Register the exception handlers
71-
register_exception_handlers(app)
76+
# Register middlewares
77+
if settings.ENABLE_AUTHENTICATION:
78+
app.add_middleware(
79+
JWTMiddleware,
80+
base_url=settings.BASE_URL,
81+
login_required_routes=login_required_routes,
82+
)
83+
# Add CORS middleware
84+
app.add_middleware(
85+
CORSMiddleware,
86+
allow_origins=settings.BACKEND_CORS_ORIGINS, # Configure appropriately for production
87+
allow_credentials=True,
88+
allow_methods=["*"],
89+
allow_headers=["*"],
90+
)
7291

7392
app.include_router(repository.router, prefix="/repository", tags=["repository"])
7493
app.include_router(issue.router, prefix="/issue", tags=["issue"])
7594

95+
if settings.ENABLE_AUTHENTICATION:
96+
app.include_router(auth.router, prefix="/auth", tags=["auth"])
97+
98+
# Register the exception handlers
99+
register_exception_handlers(app)
100+
# Register the login-required routes
101+
register_login_required_routes(app)
102+
76103

77104
@app.get("/health", tags=["health"])
78105
def health_check():
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Set, Tuple
2+
3+
from fastapi import FastAPI, Request
4+
from fastapi.security.utils import get_authorization_scheme_param
5+
from starlette.middleware.base import BaseHTTPMiddleware
6+
from starlette.responses import JSONResponse
7+
8+
from prometheus.exceptions.jwt_exception import JWTException
9+
from prometheus.utils.jwt_utils import JWTUtils
10+
11+
12+
class JWTMiddleware(BaseHTTPMiddleware):
13+
def __init__(self, app: FastAPI, base_url: str, login_required_routes: Set[Tuple[str, str]]):
14+
super().__init__(app)
15+
self.jwt_utils = JWTUtils() # Initialize the JWT utility
16+
self.login_required_routes = (
17+
login_required_routes # List of paths to exclude from JWT validation
18+
)
19+
self.base_url = base_url
20+
21+
async def dispatch(self, request: Request, call_next):
22+
# Allow OPTIONS requests to pass through without authentication (for CORS preflight)
23+
if request.method == "OPTIONS":
24+
response = await call_next(request)
25+
return response
26+
27+
# Check if the request path is in excluded paths
28+
path = request.url.path.replace(self.base_url, "")
29+
if (request.method, path) not in self.login_required_routes:
30+
# Proceed to the next middleware or route handler if the path is excluded
31+
response = await call_next(request)
32+
return response
33+
34+
# Retrieve the Authorization header from the request
35+
authorization: str = request.headers.get("Authorization")
36+
# Extract the scheme (e.g., "Bearer") and the token from the header
37+
scheme, token = get_authorization_scheme_param(authorization)
38+
39+
# Check if authorization header is missing or incorrect scheme
40+
if not authorization or scheme.lower() != "bearer":
41+
return JSONResponse(
42+
status_code=401,
43+
content={"code": 401, "message": "Valid JWT Token is missing", "data": None},
44+
)
45+
46+
try:
47+
# Attempt to decode and validate the JWT token
48+
payload = self.jwt_utils.decode_token(token)
49+
except JWTException as e:
50+
# If token validation fails, return an error response with details
51+
return JSONResponse(
52+
status_code=e.code,
53+
content={"code": e.code, "message": e.message, "data": None},
54+
)
55+
request.state.user_id = payload.get("user_id", None)
56+
# Proceed to the next middleware or route handler if validation succeeds
57+
response = await call_next(request)
58+
return response

prometheus/app/models/requests/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class LoginRequest(BaseModel):
1313
password: str = Field(
1414
description="password of the user",
1515
examples=["P@ssw0rd!"],
16-
min_length=12,
16+
min_length=8,
1717
max_length=30,
1818
)
1919

0 commit comments

Comments
 (0)