Skip to content
Closed
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
4 changes: 3 additions & 1 deletion alembic_osm/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
if config.config_file_name is not None:
fileConfig(config.config_file_name)

config.file_config.interpolation = None
escaped_url = settings.OSM_DATABASE_URL.replace("%", "%%")
# Set sqlalchemy.url from settings
config.set_main_option("sqlalchemy.url", settings.OSM_DATABASE_URL)
config.set_main_option("sqlalchemy.url", escaped_url)

# Add your model's MetaData object here for 'autogenerate' support
target_metadata = Base.metadata
Expand Down
4 changes: 3 additions & 1 deletion alembic_task/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
if config.config_file_name is not None:
fileConfig(config.config_file_name)

# config.file_config.interpolation = None
escaped_url = settings.TASK_DATABASE_URL.replace("%", "%%")
# Set sqlalchemy.url from settings
config.set_main_option("sqlalchemy.url", settings.TASK_DATABASE_URL)
config.set_main_option("sqlalchemy.url", escaped_url)

# Add your model's MetaData object here for 'autogenerate' support
target_metadata = Base.metadata
Expand Down
17 changes: 16 additions & 1 deletion api/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ def evict_user_from_cache(auth_uid: UUID) -> None:
_user_info_cache.pop(auth_uid, None)



def evict_user_from_cache(auth_uid: str) -> None:
"""
Evict a user's cached UserInfo object so that their next request re-fetches
permissions.

Call this after modifying a user's roles in the OSM DB to ensure the change
takes effect on their next request rather than after the cache TTL expires.
"""
_user_info_cache.pop(auth_uid, None)


security = HTTPBearer()


Expand Down Expand Up @@ -244,11 +256,14 @@ def isWorkspaceValidator(self, workspaceId: int) -> bool:
return True
return False

# user has has any association with a project group that owns the workspace
# user has has any association with a project group that owns the workspace,
# OR has any explicit role granted in the OSM DB for that workspace
def isWorkspaceContributor(self, workspaceId: int) -> bool:
for pgid, wsids in self.accessibleWorkspaceIds.items():
if workspaceId in wsids:
return True
if workspaceId in self.osmWorkspaceRoles:
return True
return False

def effective_role(self, workspaceId: int) -> WorkspaceUserRoleType:
Expand Down
10 changes: 10 additions & 0 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ async def lifespan(_app: FastAPI):
max_age=100,
)

# Set up CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, # Adjust this to your needs
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Include routers
app.include_router(osm_router, prefix="/api/v1")
app.include_router(teams_router, prefix="/api/v1")
Expand All @@ -115,6 +124,7 @@ async def lifespan(_app: FastAPI):
app.include_router(tasking_audit_router, prefix="/api/v1")



@app.get("/health")
async def health_check():
"""Health check endpoint. Used for Docker."""
Expand Down
29 changes: 28 additions & 1 deletion api/src/workspaces/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from api.src.workspaces.schemas import (
ImagerySettingsPatch,
QuestDefinitionType,
QuestSettingsPatch,
Workspace,
WorkspaceCreate,
WorkspaceImagery,
Expand Down Expand Up @@ -201,3 +200,31 @@ async def delete(self, current_user: UserInfo, workspace_id: int) -> None:
raise NotFoundException(f"Workspace delete failed for id {workspace_id}")

await self.session.commit()


class OSMRepository:

def __init__(self, session: AsyncSession):
self.session = session

async def getWorkspaceBBox(
self,
current_user: UserInfo,
workspace_id: int,
):
await self.session.execute(
text(f"SET search_path TO 'workspace-{workspace_id}', public")
)

sql_query = text(
"select MAX(latitude) AS max_lat, MAX(longitude) AS max_lon, \
MIN(latitude) AS min_lat, MIN(longitude) AS min_lon from nodes"
)

result = await self.session.execute(sql_query)
retVal = result.mappings().first()

if retVal is None:
raise NotFoundException(f"Workspace with id {workspace_id} not found")

return retVal
32 changes: 32 additions & 0 deletions api/src/workspaces/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from uuid import UUID
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlmodel.ext.asyncio.session import AsyncSession
Expand All @@ -16,6 +17,9 @@
from api.src.users.repository import UserRepository
from api.src.users.schemas import WorkspaceUserRoleType
from api.src.workspaces.repository import WorkspaceRepository
from api.src.users.repository import UserRepository
from api.src.users.schemas import WorkspaceUserRoleType
from api.src.workspaces.repository import OSMRepository, WorkspaceRepository
from api.src.workspaces.schemas import (
ImagerySettingsPatch,
QuestDefinitionTypeName,
Expand All @@ -24,15 +28,37 @@
WorkspaceCreate,
WorkspaceImagery,
WorkspacePatch,
WorkspaceLongQuest,
WorkspaceResponse,
)


# Set up logger for this module
logger = get_logger(__name__)

router = APIRouter(prefix="/workspaces", tags=["workspaces"])


def _to_response(workspace: Workspace, user: "UserInfo") -> WorkspaceResponse:
"""Convert a Workspace ORM object to a response model with the user's effective role."""
return WorkspaceResponse(
id=workspace.id,
type=workspace.type,
title=workspace.title,
description=workspace.description,
tdeiProjectGroupId=workspace.tdeiProjectGroupId,
tdeiRecordId=workspace.tdeiRecordId,
tdeiServiceId=workspace.tdeiServiceId,
tdeiMetadata=workspace.tdeiMetadata,
createdAt=workspace.createdAt,
createdBy=workspace.createdBy,
createdByName=workspace.createdByName,
externalAppAccess=workspace.externalAppAccess,
kartaViewToken=workspace.kartaViewToken,
role=user.effectiveRole(workspace.id),
)


def get_workspace_repository(
session: AsyncSession = Depends(get_task_session),
) -> WorkspaceRepository:
Expand All @@ -55,6 +81,12 @@ def get_user_repository(
# @test: Test that this method's results match the values of the fetch-by-workspace-id method below; all workspaces in this list are retrievable via that method


def get_user_repository(
session: AsyncSession = Depends(get_osm_session),
) -> UserRepository:
return UserRepository(session)


# Returns list of workspaces user has access to as JSON payload on success--returns empty JSON list if none
@router.get("/mine", response_model=list[WorkspaceResponse])
async def get_my_workspaces(
Expand Down
1 change: 1 addition & 0 deletions api/src/workspaces/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime
from enum import IntEnum, StrEnum
from typing import TYPE_CHECKING, Any, Optional, Self
from typing import Any, Optional
from uuid import UUID

from geoalchemy2 import Geometry
Expand Down