Skip to content

Commit

Permalink
auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Jairo Matos Da Rocha committed Sep 29, 2024
1 parent 11adc10 commit 4256e59
Show file tree
Hide file tree
Showing 12 changed files with 523 additions and 90 deletions.
47 changes: 47 additions & 0 deletions app/api/oauth2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
import requests


from fastapi import APIRouter, Depends, HTTPException


from app.oauth2 import CLIENT_ID, CLIENT_SECRET, TOKEN_URL


routes = APIRouter()


# Modelo de resposta para o token
class Token(BaseModel):
access_token: str
token_type: str

# Rota de login para obter o token de acesso
@routes.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Dados para a requisição ao Keycloak
payload = {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'grant_type': 'password',
'username': form_data.username,
'password': form_data.password
}

# Requisição ao Keycloak para obter o token
response = requests.post(TOKEN_URL, data=payload)

if response.status_code != 200:
raise HTTPException(
status_code=401,
detail="Username or password incorrect"
)

token_data = response.json()
return {
"access_token": token_data["access_token"],
"token_type": "bearer"
}


2 changes: 1 addition & 1 deletion app/api/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
router = APIRouter()


@router.get("/pasture/{task_id}")
@router.get("/{task_id}")
async def result_pasture(task_id):
with MongoClient(os.environ.get("MONGOURI", "mongodb://mongodbjobs:27017")) as cliente:
# Seleciona o banco de dados e a coleção
Expand Down
27 changes: 19 additions & 8 deletions app/api/task.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@

from fastapi.responses import JSONResponse
import geopandas as gpd


from app.models.oauth2 import UserInfo
from app.oauth2 import has_role
from worker import gee_get_index_pasture
from app.models.payload import PayloadSaveGeojson
import os

from fastapi import APIRouter, HTTPException, Request, Query
from fastapi import APIRouter, Depends, HTTPException, Request, Query
from app.config import logger
from celery.result import AsyncResult
from pydantic import UUID4

router = APIRouter()

@router.post("/savegeom")

@router.post("/savegeom" )
async def savegeom(
payload: PayloadSaveGeojson,
request: Request,
crs: int = Query(4326, description="EPSG code for the geometry"),

user_data: UserInfo = Depends(has_role(['savegeom'])) # This should be a function that retrieves user data from the request.
):
MAX_HECTARES = 40_000

MAXHECTARES = os.environ.get('MAXHECTARES',40_000)
geojson = payload.dict().get('geojson',gpd.GeoDataFrame())
logger.info(f"Received payload: {geojson}")
try:
Expand All @@ -27,11 +33,16 @@ async def savegeom(
return HTTPException(status_code=400, detail="Empty GeoDataFrame or more than one feature.")
if gdf.geometry.type[0] != "Polygon":
return HTTPException(status_code=400, detail="Geometry must be a Polygon.")
if gdf.to_crs(5880).area.iloc[0] / 10_000 > MAX_HECTARES:
return HTTPException(status_code=400, detail=f"Geometry area must be less than {MAX_HECTARES} hectares.")
if gdf.to_crs(5880).area.iloc[0] / 10_000 > MAXHECTARES:
return HTTPException(status_code=400, detail=f"Geometry area must be less than {MAXHECTARES} hectares.")
logger.info('Geometry is valid')
logger.info(user_data)
try:
task = gee_get_index_pasture.delay(payload.dict())
dict_payload = {
**payload.dict(),
'request_user': user_data
}
task = gee_get_index_pasture.delay(dict_payload)
except Exception as e:
logger.exception(f"Failed to create task: {e}")
raise HTTPException(status_code=400, detail="Failed to create task.")
Expand Down
19 changes: 19 additions & 0 deletions app/models/oauth2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Optional
from pydantic import BaseModel, EmailStr

class UserOauth2(BaseModel):
id: str
username: str
email: str
first_name: str
last_name: str
realm_roles: list
client_roles: list



class UserInfo(BaseModel):
sub: str
preferred_username: str
email: Optional[EmailStr] = None
client_id: Optional[str] = None
6 changes: 6 additions & 0 deletions app/models/payload.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pydantic import BaseModel, EmailStr
from typing import List
from app.models.oauth2 import UserInfo
from pydantic_geojson import FeatureModel, FeatureCollectionModel, PolygonModel

class User(BaseModel):
Expand All @@ -15,6 +16,11 @@ class LapigFeatureModel(FeatureModel):
class LapigFeatureCollectionModel(FeatureCollectionModel):
features: List[LapigFeatureModel]


class ResultPayload(BaseModel):
user: User
geojson: LapigFeatureCollectionModel
request_user: UserInfo
class PayloadSaveGeojson(BaseModel):
user: User
geojson: LapigFeatureCollectionModel
120 changes: 120 additions & 0 deletions app/oauth2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from typing import List
from fastapi.security import OAuth2AuthorizationCodeBearer
from keycloak import KeycloakOpenID # pip require python-keycloak
from fastapi import Security, HTTPException, status,Depends
from pydantic import Json
from app.models.oauth2 import UserInfo, UserOauth2

import os
from app.config import logger

URLKEYCLOAK = f"{os.environ.get('SERVER_URL')}/realms/{os.environ.get('REALM')}/"


TOKEN_URL = f"{URLKEYCLOAK}protocol/openid-connect/token"
CLIENT_ID = os.environ.get('CLIENT_ID')
CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
AUTHORIZATIONURL=f"{URLKEYCLOAK}protocol/openid-connect/auth"
CERTS=f"{URLKEYCLOAK}protocol/openid-connect/certs"
REALM=os.environ.get('REALM')

# This is used for fastapi docs authentification
oauth2_scheme = OAuth2AuthorizationCodeBearer(
#refreshUrl=TOKEN_URL,
authorizationUrl=AUTHORIZATIONURL, # https://sso.example.com/auth/
tokenUrl=TOKEN_URL, # https://sso.example.com/auth/realms/example-realm/protocol/openid-connect/token
)

# This actually does the auth checks
# client_secret_key is not mandatory if the client is public on keycloak
keycloak_openid = KeycloakOpenID(
server_url=os.environ.get('SERVER_URL'), # https://sso.example.com/auth/
client_id=CLIENT_ID, # backend-client-id
realm_name=REALM, # example-realm
client_secret_key=CLIENT_SECRET, # your backend client secret
verify=True
)





async def get_idp_public_key():
return (
"-----BEGIN PUBLIC KEY-----\n"
f"{keycloak_openid.public_key()}"
"\n-----END PUBLIC KEY-----"
)

# Get the payload/token from keycloak
async def get_payload(token: str = Security(oauth2_scheme)) -> dict:
key= await get_idp_public_key()
logger.info(f"token {token}")
try:

return keycloak_openid.decode_token(
token
)
except Exception as e:
logger.exception(f"Error decoding token: {e}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e), # "Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)

def get_info_user(payload) -> UserInfo:
if not payload.get('email', None) is None:
return {
'sub': payload['sub'],
'preferred_username': payload['preferred_username'],
'email': payload['email']
}
else:
return {
'sub': payload['sub'],
'preferred_username': payload['preferred_username'],
'client_id': payload.get('client_id')
}



def has_role(role:List[str]) -> UserInfo:
logger.info(f"has_role {role}")
async def check_role(payload: dict = Depends(get_payload)):

if not payload.get("resource_access", {}).get("app_task", {}):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized to access this resource",
headers={"WWW-Authenticate": "Bearer"},
)
if not any([user_role in role for user_role in payload.get("resource_access", {}).get("app_task", {}).get("roles", [])]):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient privileges to access this resource",
headers={"WWW-Authenticate": "Bearer"},
)

return get_info_user(payload)
return check_role


# Get user infos from the payload
async def get_user_info(payload: dict = Depends(get_payload)) -> UserOauth2:
try:
return UserOauth2(
id=payload.get("sub"),
username=payload.get("preferred_username"),
email=payload.get("email"),
first_name=payload.get("given_name"),
last_name=payload.get("family_name"),
realm_roles=payload.get("realm_access", {}).get("roles", []),
client_roles=payload.get("realm_access", {}).get("roles", [])
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e), # "Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
3 changes: 2 additions & 1 deletion app/router.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from .api import task, result
from .api import task, result, oauth2


def created_routes(app):

app.include_router(task.router, prefix="/api/task", tags=["Task Google Earth Engine"])
app.include_router(result.router, prefix="/api/result", tags=["Result Google Earth Engine"])
app.include_router(oauth2.routes, prefix="/api/auth", tags=["Authentication"])

return app
15 changes: 11 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ services:
context: .
dockerfile: ./docker/prod/Dockerfile
hostname: jobsgee
command: uv run uvicorn main:app --host 0.0.0.0 --port 8080 --reload
command: uv run uvicorn main:app --host 0.0.0.0 --port 8080 --reload
environment:
- HTTPS=false
- CELERY_BROKER_URL=redis://queejobs:6379/0
- CELERY_RESULT_BACKEND=redis://queejobs:6379/0
- SERVER_URL=${SERVER_URL}
- CLIENT_ID=${CLIENT_ID}
- CLIENT_SECRET=${CLIENT_SECRET}
- ADMIN_CLIENT_SECRET=${ADMIN_CLIENT_SECRET}
- REALM=${REALM}
- CALLBACK_URI=${CALLBACK_URI}
container_name: jobsgee
privileged: true
ports:
- "8086:8080"
volumes:
- .:/home/lapig
- .:/home/suporte

networks:
- web_lapig
Expand All @@ -24,7 +31,7 @@ services:
dockerfile: ./docker/prod/Dockerfile
command: uv run celery -A worker.celery worker --loglevel=info --logfile=logs/celery.log
volumes:
- .:/home/lapig
- .:/home/suporte
- ./volumes/gee.json:/var/sec/gee.json
- ./volumes/logs:/logs
environment:
Expand All @@ -42,7 +49,7 @@ services:
ports:
- 5556:5555
volumes:
- .:/home/lapig
- .:/home/suporte
environment:
- CELERY_BROKER_URL=redis://queejobs:6379/0
- CELERY_RESULT_BACKEND=redis://queejobs:6379/0
Expand Down
Loading

0 comments on commit 4256e59

Please sign in to comment.