first commit
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
"""API routes."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from . import auth, users, tenants, gateways, endpoints, connections
|
||||
|
||||
# Create main API router
|
||||
api_router = APIRouter(prefix="/api")
|
||||
|
||||
# Include all route modules
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
api_router.include_router(tenants.router, prefix="/tenants", tags=["Tenants"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
api_router.include_router(gateways.router, prefix="/gateways", tags=["Gateways"])
|
||||
api_router.include_router(endpoints.router, prefix="/endpoints", tags=["Endpoints"])
|
||||
api_router.include_router(connections.router, prefix="/connections", tags=["Connections"])
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Authentication API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..schemas.user import UserLogin, Token, UserResponse
|
||||
from ..services.auth_service import AuthService
|
||||
from .deps import get_current_user
|
||||
from ..models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
def login(
|
||||
credentials: UserLogin,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Authenticate user and return JWT tokens."""
|
||||
auth_service = AuthService(db)
|
||||
user = auth_service.authenticate_user(credentials.username, credentials.password)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
return auth_service.create_tokens(user)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=Token)
|
||||
def refresh_token(
|
||||
refresh_token: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Refresh access token using refresh token."""
|
||||
auth_service = AuthService(db)
|
||||
tokens = auth_service.refresh_tokens(refresh_token)
|
||||
|
||||
if not tokens:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
return tokens
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_current_user_info(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get current authenticated user information."""
|
||||
return current_user
|
||||
@@ -0,0 +1,236 @@
|
||||
"""Connection management API routes."""
|
||||
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.endpoint import Endpoint
|
||||
from ..models.gateway import Gateway
|
||||
from ..models.user import User, UserRole
|
||||
from ..models.access import UserGatewayAccess, ConnectionLog
|
||||
from ..services.vpn_service import VPNService
|
||||
from ..services.firewall_service import FirewallService
|
||||
from .deps import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ConnectRequest(BaseModel):
|
||||
"""Request to establish connection to endpoint."""
|
||||
gateway_id: int
|
||||
endpoint_id: int
|
||||
|
||||
|
||||
class ConnectResponse(BaseModel):
|
||||
"""Response with connection details."""
|
||||
success: bool
|
||||
message: str
|
||||
vpn_config: str | None = None
|
||||
target_ip: str | None = None
|
||||
target_port: int | None = None
|
||||
connection_id: int | None = None
|
||||
|
||||
|
||||
class DisconnectRequest(BaseModel):
|
||||
"""Request to disconnect from endpoint."""
|
||||
connection_id: int
|
||||
|
||||
|
||||
def user_has_gateway_access(db: Session, user: User, gateway_id: int) -> bool:
|
||||
"""Check if user has access to gateway."""
|
||||
if user.role in (UserRole.SUPER_ADMIN, UserRole.ADMIN):
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if user.role == UserRole.SUPER_ADMIN:
|
||||
return gateway is not None
|
||||
return gateway and gateway.tenant_id == user.tenant_id
|
||||
|
||||
access = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.user_id == user.id,
|
||||
UserGatewayAccess.gateway_id == gateway_id
|
||||
).first()
|
||||
return access is not None
|
||||
|
||||
|
||||
@router.post("/connect", response_model=ConnectResponse)
|
||||
def connect_to_endpoint(
|
||||
request: ConnectRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Request connection to a specific endpoint through a gateway."""
|
||||
# Check gateway access
|
||||
if not user_has_gateway_access(db, current_user, request.gateway_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="No access to this gateway"
|
||||
)
|
||||
|
||||
# Get gateway
|
||||
gateway = db.query(Gateway).filter(Gateway.id == request.gateway_id).first()
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found"
|
||||
)
|
||||
|
||||
if not gateway.is_online:
|
||||
return ConnectResponse(
|
||||
success=False,
|
||||
message="Gateway is offline"
|
||||
)
|
||||
|
||||
# Get endpoint
|
||||
endpoint = db.query(Endpoint).filter(
|
||||
Endpoint.id == request.endpoint_id,
|
||||
Endpoint.gateway_id == request.gateway_id
|
||||
).first()
|
||||
|
||||
if not endpoint:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Endpoint not found"
|
||||
)
|
||||
|
||||
# NOTE: Dynamic VPN config generation has been replaced by VPN profiles.
|
||||
# Gateways should have pre-provisioned VPN profiles.
|
||||
# This endpoint now just logs the connection intent.
|
||||
|
||||
# Log connection
|
||||
connection = ConnectionLog(
|
||||
user_id=current_user.id,
|
||||
gateway_id=gateway.id,
|
||||
endpoint_id=endpoint.id,
|
||||
client_ip=None # Will be updated when VPN connects
|
||||
)
|
||||
db.add(connection)
|
||||
db.commit()
|
||||
db.refresh(connection)
|
||||
|
||||
return ConnectResponse(
|
||||
success=True,
|
||||
message="Connection logged. Use the gateway's VPN profile configuration to connect.",
|
||||
vpn_config=None, # VPN config is now obtained through gateway VPN profiles
|
||||
target_ip=endpoint.internal_ip,
|
||||
target_port=endpoint.port,
|
||||
connection_id=connection.id
|
||||
)
|
||||
|
||||
|
||||
@router.post("/disconnect")
|
||||
def disconnect_from_endpoint(
|
||||
request: DisconnectRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Disconnect from endpoint."""
|
||||
connection = db.query(ConnectionLog).filter(
|
||||
ConnectionLog.id == request.connection_id,
|
||||
ConnectionLog.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
)
|
||||
|
||||
if connection.disconnected_at:
|
||||
return {"message": "Already disconnected"}
|
||||
|
||||
# Revoke firewall rules if VPN IP is known
|
||||
if connection.vpn_ip:
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == connection.endpoint_id).first()
|
||||
gateway = db.query(Gateway).filter(Gateway.id == connection.gateway_id).first()
|
||||
|
||||
if endpoint and gateway:
|
||||
firewall = FirewallService()
|
||||
firewall.revoke_connection(
|
||||
client_vpn_ip=connection.vpn_ip,
|
||||
gateway_vpn_ip=gateway.vpn_ip,
|
||||
target_ip=endpoint.internal_ip,
|
||||
target_port=endpoint.port,
|
||||
protocol=endpoint.protocol.value
|
||||
)
|
||||
|
||||
# Disconnect VPN client
|
||||
vpn_service = VPNService()
|
||||
client_cn = f"client-{current_user.username}-{current_user.id}"
|
||||
vpn_service.disconnect_client(client_cn)
|
||||
|
||||
# Update log
|
||||
connection.disconnected_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {"message": "Disconnected successfully"}
|
||||
|
||||
|
||||
@router.get("/active")
|
||||
def list_active_connections(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List active connections for current user."""
|
||||
connections = db.query(ConnectionLog).filter(
|
||||
ConnectionLog.user_id == current_user.id,
|
||||
ConnectionLog.disconnected_at.is_(None)
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for conn in connections:
|
||||
gateway = db.query(Gateway).filter(Gateway.id == conn.gateway_id).first()
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == conn.endpoint_id).first()
|
||||
|
||||
result.append({
|
||||
"connection_id": conn.id,
|
||||
"gateway_name": gateway.name if gateway else None,
|
||||
"endpoint_name": endpoint.name if endpoint else None,
|
||||
"endpoint_address": endpoint.address if endpoint else None,
|
||||
"connected_at": conn.connected_at.isoformat(),
|
||||
"vpn_ip": conn.vpn_ip
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
def get_connection_history(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get connection history for current user."""
|
||||
query = db.query(ConnectionLog).filter(ConnectionLog.user_id == current_user.id)
|
||||
|
||||
# Admins can see all connections in their tenant
|
||||
if current_user.is_admin:
|
||||
if current_user.role == UserRole.SUPER_ADMIN:
|
||||
query = db.query(ConnectionLog)
|
||||
else:
|
||||
# Filter by tenant's gateways
|
||||
query = db.query(ConnectionLog).join(
|
||||
Gateway,
|
||||
Gateway.id == ConnectionLog.gateway_id
|
||||
).filter(Gateway.tenant_id == current_user.tenant_id)
|
||||
|
||||
connections = query.order_by(ConnectionLog.connected_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for conn in connections:
|
||||
user = db.query(User).filter(User.id == conn.user_id).first()
|
||||
gateway = db.query(Gateway).filter(Gateway.id == conn.gateway_id).first()
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == conn.endpoint_id).first()
|
||||
|
||||
result.append({
|
||||
"connection_id": conn.id,
|
||||
"username": user.username if user else None,
|
||||
"gateway_name": gateway.name if gateway else None,
|
||||
"endpoint_name": endpoint.name if endpoint else None,
|
||||
"connected_at": conn.connected_at.isoformat(),
|
||||
"disconnected_at": conn.disconnected_at.isoformat() if conn.disconnected_at else None,
|
||||
"duration_seconds": conn.duration_seconds,
|
||||
"client_ip": conn.client_ip
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,87 @@
|
||||
"""API dependencies for authentication and authorization."""
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User, UserRole
|
||||
from ..utils.security import decode_token
|
||||
|
||||
# Bearer token security scheme
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
"""Get current authenticated user from JWT token."""
|
||||
token = credentials.credentials
|
||||
payload = decode_token(token)
|
||||
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired token",
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
if payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token type",
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User account is deactivated"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> User:
|
||||
"""Ensure user is active."""
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Inactive user"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
def require_admin(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> User:
|
||||
"""Require admin role."""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
def require_super_admin(
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> User:
|
||||
"""Require super admin role."""
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Super admin privileges required"
|
||||
)
|
||||
return current_user
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Endpoint management API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.endpoint import Endpoint, ApplicationTemplate
|
||||
from ..models.gateway import Gateway
|
||||
from ..models.user import User, UserRole
|
||||
from ..models.access import UserGatewayAccess, UserEndpointAccess
|
||||
from ..schemas.endpoint import (
|
||||
EndpointCreate, EndpointUpdate, EndpointResponse,
|
||||
ApplicationTemplateResponse
|
||||
)
|
||||
from .deps import get_current_user, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def user_has_gateway_access(db: Session, user: User, gateway_id: int) -> bool:
|
||||
"""Check if user has access to gateway."""
|
||||
if user.role == UserRole.SUPER_ADMIN:
|
||||
return True
|
||||
if user.role == UserRole.ADMIN:
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
return gateway and gateway.tenant_id == user.tenant_id
|
||||
|
||||
access = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.user_id == user.id,
|
||||
UserGatewayAccess.gateway_id == gateway_id
|
||||
).first()
|
||||
return access is not None
|
||||
|
||||
|
||||
@router.get("/templates", response_model=list[ApplicationTemplateResponse])
|
||||
def list_application_templates(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List available application templates."""
|
||||
templates = db.query(ApplicationTemplate).all()
|
||||
return templates
|
||||
|
||||
|
||||
@router.get("/gateway/{gateway_id}", response_model=list[EndpointResponse])
|
||||
def list_endpoints_for_gateway(
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List endpoints for a gateway."""
|
||||
if not user_has_gateway_access(db, current_user, gateway_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="No access to this gateway"
|
||||
)
|
||||
|
||||
endpoints = db.query(Endpoint).filter(Endpoint.gateway_id == gateway_id).all()
|
||||
return endpoints
|
||||
|
||||
|
||||
@router.post("/gateway/{gateway_id}", response_model=EndpointResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_endpoint(
|
||||
gateway_id: int,
|
||||
endpoint_data: EndpointCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Create a new endpoint for a gateway."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found"
|
||||
)
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if gateway.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot add endpoints to gateways from other tenants"
|
||||
)
|
||||
|
||||
endpoint = Endpoint(
|
||||
**endpoint_data.model_dump(),
|
||||
gateway_id=gateway_id
|
||||
)
|
||||
db.add(endpoint)
|
||||
db.commit()
|
||||
db.refresh(endpoint)
|
||||
return endpoint
|
||||
|
||||
|
||||
@router.get("/{endpoint_id}", response_model=EndpointResponse)
|
||||
def get_endpoint(
|
||||
endpoint_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get endpoint by ID."""
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == endpoint_id).first()
|
||||
if not endpoint:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Endpoint not found"
|
||||
)
|
||||
|
||||
if not user_has_gateway_access(db, current_user, endpoint.gateway_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="No access to this endpoint"
|
||||
)
|
||||
|
||||
return endpoint
|
||||
|
||||
|
||||
@router.put("/{endpoint_id}", response_model=EndpointResponse)
|
||||
def update_endpoint(
|
||||
endpoint_id: int,
|
||||
endpoint_data: EndpointUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Update endpoint."""
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == endpoint_id).first()
|
||||
if not endpoint:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Endpoint not found"
|
||||
)
|
||||
|
||||
gateway = db.query(Gateway).filter(Gateway.id == endpoint.gateway_id).first()
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if gateway.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot modify endpoints from other tenants"
|
||||
)
|
||||
|
||||
update_data = endpoint_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(endpoint, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(endpoint)
|
||||
return endpoint
|
||||
|
||||
|
||||
@router.delete("/{endpoint_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_endpoint(
|
||||
endpoint_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Delete endpoint."""
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == endpoint_id).first()
|
||||
if not endpoint:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Endpoint not found"
|
||||
)
|
||||
|
||||
gateway = db.query(Gateway).filter(Gateway.id == endpoint.gateway_id).first()
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if gateway.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot delete endpoints from other tenants"
|
||||
)
|
||||
|
||||
db.delete(endpoint)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.post("/{endpoint_id}/access/{user_id}", status_code=status.HTTP_201_CREATED)
|
||||
def grant_endpoint_access(
|
||||
endpoint_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Grant user access to specific endpoint (fine-grained control)."""
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == endpoint_id).first()
|
||||
if not endpoint:
|
||||
raise HTTPException(status_code=404, detail="Endpoint not found")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
existing = db.query(UserEndpointAccess).filter(
|
||||
UserEndpointAccess.endpoint_id == endpoint_id,
|
||||
UserEndpointAccess.user_id == user_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Access already granted")
|
||||
|
||||
access = UserEndpointAccess(
|
||||
user_id=user_id,
|
||||
endpoint_id=endpoint_id,
|
||||
granted_by_id=current_user.id
|
||||
)
|
||||
db.add(access)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Access granted"}
|
||||
|
||||
|
||||
@router.delete("/{endpoint_id}/access/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def revoke_endpoint_access(
|
||||
endpoint_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Revoke user access to endpoint."""
|
||||
access = db.query(UserEndpointAccess).filter(
|
||||
UserEndpointAccess.endpoint_id == endpoint_id,
|
||||
UserEndpointAccess.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not access:
|
||||
raise HTTPException(status_code=404, detail="Access not found")
|
||||
|
||||
db.delete(access)
|
||||
db.commit()
|
||||
@@ -0,0 +1,335 @@
|
||||
"""Gateway management API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.gateway import Gateway
|
||||
from ..models.user import User, UserRole
|
||||
from ..models.access import UserGatewayAccess
|
||||
from ..models.vpn_connection_log import VPNConnectionLog
|
||||
from ..schemas.gateway import GatewayCreate, GatewayUpdate, GatewayResponse, GatewayStatus
|
||||
from ..services.vpn_sync_service import VPNSyncService
|
||||
from .deps import get_current_user, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def get_accessible_gateways_query(db: Session, user: User):
|
||||
"""Get query for gateways accessible by user."""
|
||||
if user.role == UserRole.SUPER_ADMIN:
|
||||
return db.query(Gateway)
|
||||
elif user.role == UserRole.ADMIN:
|
||||
return db.query(Gateway).filter(Gateway.tenant_id == user.tenant_id)
|
||||
else:
|
||||
# Technicians and viewers only see assigned gateways
|
||||
return db.query(Gateway).join(
|
||||
UserGatewayAccess,
|
||||
UserGatewayAccess.gateway_id == Gateway.id
|
||||
).filter(UserGatewayAccess.user_id == user.id)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[GatewayResponse])
|
||||
def list_gateways(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""List gateways accessible by current user."""
|
||||
query = get_accessible_gateways_query(db, current_user)
|
||||
gateways = query.offset(skip).limit(limit).all()
|
||||
return gateways
|
||||
|
||||
|
||||
@router.get("/status", response_model=list[GatewayStatus])
|
||||
def get_gateways_status(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get online status of all accessible gateways."""
|
||||
query = get_accessible_gateways_query(db, current_user)
|
||||
gateways = query.all()
|
||||
|
||||
return [
|
||||
GatewayStatus(
|
||||
id=gw.id,
|
||||
name=gw.name,
|
||||
is_online=gw.is_online,
|
||||
last_seen=gw.last_seen,
|
||||
vpn_ip=gw.vpn_ip
|
||||
)
|
||||
for gw in gateways
|
||||
]
|
||||
|
||||
|
||||
@router.post("/", response_model=GatewayResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_gateway(
|
||||
gateway_data: GatewayCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Create a new gateway (admin only)."""
|
||||
# Determine tenant_id
|
||||
if current_user.role == UserRole.SUPER_ADMIN:
|
||||
# Super admin must specify tenant_id via query param or we use a default
|
||||
tenant_id = current_user.tenant_id or 1 # Fallback
|
||||
else:
|
||||
tenant_id = current_user.tenant_id
|
||||
|
||||
# Generate VPN certificate CN
|
||||
vpn_cert_cn = f"gateway-{gateway_data.name.lower().replace(' ', '-')}"
|
||||
|
||||
gateway = Gateway(
|
||||
**gateway_data.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
vpn_cert_cn=vpn_cert_cn
|
||||
)
|
||||
db.add(gateway)
|
||||
db.commit()
|
||||
db.refresh(gateway)
|
||||
return gateway
|
||||
|
||||
|
||||
@router.get("/{gateway_id}", response_model=GatewayResponse)
|
||||
def get_gateway(
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get gateway by ID."""
|
||||
query = get_accessible_gateways_query(db, current_user)
|
||||
gateway = query.filter(Gateway.id == gateway_id).first()
|
||||
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found or access denied"
|
||||
)
|
||||
|
||||
return gateway
|
||||
|
||||
|
||||
@router.put("/{gateway_id}", response_model=GatewayResponse)
|
||||
def update_gateway(
|
||||
gateway_id: int,
|
||||
gateway_data: GatewayUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Update gateway (admin only)."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found"
|
||||
)
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if gateway.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot modify gateways from other tenants"
|
||||
)
|
||||
|
||||
update_data = gateway_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(gateway, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(gateway)
|
||||
return gateway
|
||||
|
||||
|
||||
@router.delete("/{gateway_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_gateway(
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Delete gateway (admin only)."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found"
|
||||
)
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if gateway.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot delete gateways from other tenants"
|
||||
)
|
||||
|
||||
db.delete(gateway)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("/{gateway_id}/provision")
|
||||
def download_provisioning_package(
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Download provisioning package for gateway.
|
||||
|
||||
DEPRECATED: Use VPN profiles for provisioning.
|
||||
GET /api/gateways/{gateway_id}/profiles/{profile_id}/provision
|
||||
"""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found"
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Provisioning is now done through VPN profiles. "
|
||||
"Please create a VPN profile for this gateway first, "
|
||||
"then use GET /api/gateways/{gateway_id}/profiles/{profile_id}/provision"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{gateway_id}/access/{user_id}", status_code=status.HTTP_201_CREATED)
|
||||
def grant_gateway_access(
|
||||
gateway_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Grant user access to gateway."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
raise HTTPException(status_code=404, detail="Gateway not found")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Check existing access
|
||||
existing = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.gateway_id == gateway_id,
|
||||
UserGatewayAccess.user_id == user_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Access already granted")
|
||||
|
||||
access = UserGatewayAccess(
|
||||
user_id=user_id,
|
||||
gateway_id=gateway_id,
|
||||
granted_by_id=current_user.id
|
||||
)
|
||||
db.add(access)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Access granted"}
|
||||
|
||||
|
||||
@router.delete("/{gateway_id}/access/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def revoke_gateway_access(
|
||||
gateway_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Revoke user access to gateway."""
|
||||
access = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.gateway_id == gateway_id,
|
||||
UserGatewayAccess.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not access:
|
||||
raise HTTPException(status_code=404, detail="Access not found")
|
||||
|
||||
db.delete(access)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("/{gateway_id}/vpn-logs")
|
||||
def get_gateway_vpn_logs(
|
||||
gateway_id: int,
|
||||
limit: int = 50,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get VPN connection logs for a gateway."""
|
||||
# Check access
|
||||
query = get_accessible_gateways_query(db, current_user)
|
||||
gateway = query.filter(Gateway.id == gateway_id).first()
|
||||
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found or access denied"
|
||||
)
|
||||
|
||||
sync_service = VPNSyncService(db)
|
||||
logs = sync_service.get_gateway_connection_logs(gateway_id, limit=limit)
|
||||
|
||||
return [
|
||||
{
|
||||
"id": log.id,
|
||||
"profile_id": log.vpn_profile_id,
|
||||
"profile_name": log.vpn_profile.name if log.vpn_profile else None,
|
||||
"server_id": log.vpn_server_id,
|
||||
"server_name": log.vpn_server.name if log.vpn_server else None,
|
||||
"common_name": log.common_name,
|
||||
"real_address": log.real_address,
|
||||
"vpn_ip": log.vpn_ip,
|
||||
"connected_at": log.connected_at.isoformat() if log.connected_at else None,
|
||||
"disconnected_at": log.disconnected_at.isoformat() if log.disconnected_at else None,
|
||||
"duration_seconds": log.duration_seconds,
|
||||
"bytes_received": log.bytes_received,
|
||||
"bytes_sent": log.bytes_sent,
|
||||
"is_active": log.is_active
|
||||
}
|
||||
for log in logs
|
||||
]
|
||||
|
||||
|
||||
@router.get("/{gateway_id}/vpn-status")
|
||||
def get_gateway_vpn_status(
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get current VPN connection status for a gateway."""
|
||||
# Check access
|
||||
query = get_accessible_gateways_query(db, current_user)
|
||||
gateway = query.filter(Gateway.id == gateway_id).first()
|
||||
|
||||
if not gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found or access denied"
|
||||
)
|
||||
|
||||
# Sync connections first
|
||||
sync_service = VPNSyncService(db)
|
||||
sync_service.sync_all_connections()
|
||||
|
||||
# Get active connection
|
||||
active_log = db.query(VPNConnectionLog).filter(
|
||||
VPNConnectionLog.gateway_id == gateway_id,
|
||||
VPNConnectionLog.disconnected_at.is_(None)
|
||||
).first()
|
||||
|
||||
if active_log:
|
||||
return {
|
||||
"is_connected": True,
|
||||
"profile_name": active_log.vpn_profile.name if active_log.vpn_profile else None,
|
||||
"server_name": active_log.vpn_server.name if active_log.vpn_server else None,
|
||||
"real_address": active_log.real_address,
|
||||
"connected_since": active_log.connected_at.isoformat() if active_log.connected_at else None,
|
||||
"bytes_received": active_log.bytes_received,
|
||||
"bytes_sent": active_log.bytes_sent
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"is_connected": False,
|
||||
"last_connection": gateway.last_seen.isoformat() if gateway.last_seen else None
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
"""Internal API endpoints for container-to-container communication.
|
||||
|
||||
These endpoints are used by OpenVPN containers to fetch their configuration.
|
||||
They should only be accessible from within the Docker network.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.vpn_server import VPNServer, VPNServerStatus
|
||||
from ..models.vpn_profile import VPNProfile
|
||||
from ..services.vpn_server_service import VPNServerService
|
||||
from ..services.vpn_sync_service import VPNSyncService
|
||||
from ..services.certificate_service import CertificateService
|
||||
|
||||
# Log directory (shared volume with OpenVPN container)
|
||||
LOG_DIR = Path("/var/log/openvpn")
|
||||
|
||||
router = APIRouter(prefix="/api/internal", tags=["internal"])
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint for container startup."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
@router.get("/vpn-servers/active")
|
||||
async def get_active_servers(db: Session = Depends(get_db)):
|
||||
"""Get list of all active and ready VPN servers.
|
||||
|
||||
Used by OpenVPN container to discover which servers to start.
|
||||
"""
|
||||
servers = db.query(VPNServer).filter(
|
||||
VPNServer.is_active == True
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"port": s.port,
|
||||
"protocol": s.protocol.value,
|
||||
"management_port": s.management_port,
|
||||
"vpn_network": s.vpn_network,
|
||||
"vpn_netmask": s.vpn_netmask,
|
||||
"is_ready": s.is_ready,
|
||||
"has_ca": s.certificate_authority is not None and s.certificate_authority.is_ready,
|
||||
"has_cert": s.server_cert is not None and s.server_key is not None
|
||||
}
|
||||
for s in servers
|
||||
]
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/config")
|
||||
async def get_server_config(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get OpenVPN server configuration file."""
|
||||
service = VPNServerService(db)
|
||||
server = service.get_server_by_id(server_id)
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if not server.server_cert or not server.server_key:
|
||||
raise HTTPException(status_code=400, detail="Server certificate not generated")
|
||||
|
||||
config = service.generate_server_config(server)
|
||||
|
||||
# Update server status
|
||||
server.status = VPNServerStatus.STARTING
|
||||
db.commit()
|
||||
|
||||
return Response(content=config, media_type="text/plain")
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/ca")
|
||||
async def get_server_ca(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get CA certificate for a VPN server."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if not server.certificate_authority or not server.certificate_authority.ca_cert:
|
||||
raise HTTPException(status_code=400, detail="CA not available")
|
||||
|
||||
return Response(
|
||||
content=server.certificate_authority.ca_cert,
|
||||
media_type="application/x-pem-file"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/cert")
|
||||
async def get_server_cert(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get server certificate."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if not server.server_cert:
|
||||
raise HTTPException(status_code=400, detail="Server certificate not generated")
|
||||
|
||||
return Response(
|
||||
content=server.server_cert,
|
||||
media_type="application/x-pem-file"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/key")
|
||||
async def get_server_key(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get server private key."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if not server.server_key:
|
||||
raise HTTPException(status_code=400, detail="Server key not generated")
|
||||
|
||||
return Response(
|
||||
content=server.server_key,
|
||||
media_type="application/x-pem-file"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/dh")
|
||||
async def get_server_dh(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get DH parameters."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if not server.certificate_authority or not server.certificate_authority.dh_params:
|
||||
raise HTTPException(status_code=400, detail="DH parameters not available")
|
||||
|
||||
return Response(
|
||||
content=server.certificate_authority.dh_params,
|
||||
media_type="application/x-pem-file"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/ta")
|
||||
async def get_server_ta(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get TLS-Auth key."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if not server.ta_key:
|
||||
raise HTTPException(status_code=400, detail="TA key not available")
|
||||
|
||||
return Response(
|
||||
content=server.ta_key,
|
||||
media_type="text/plain"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/crl")
|
||||
async def get_server_crl(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get Certificate Revocation List."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if not server.certificate_authority:
|
||||
raise HTTPException(status_code=400, detail="CA not available")
|
||||
|
||||
cert_service = CertificateService(db)
|
||||
crl = cert_service.get_crl(server.certificate_authority)
|
||||
|
||||
return Response(
|
||||
content=crl,
|
||||
media_type="application/x-pem-file"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/vpn-servers/{server_id}/started")
|
||||
async def notify_server_started(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Notify that server has started successfully."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
server.status = VPNServerStatus.RUNNING
|
||||
db.commit()
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/vpn-servers/{server_id}/stopped")
|
||||
async def notify_server_stopped(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Notify that server has stopped."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
server.status = VPNServerStatus.STOPPED
|
||||
db.commit()
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/logs")
|
||||
async def get_server_logs(
|
||||
server_id: int,
|
||||
lines: int = Query(default=100, le=1000),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get OpenVPN server log file.
|
||||
|
||||
Args:
|
||||
server_id: VPN server ID
|
||||
lines: Number of lines to return (max 1000)
|
||||
"""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
log_file = LOG_DIR / f"server-{server_id}.log"
|
||||
|
||||
if not log_file.exists():
|
||||
return {"lines": [], "message": "Log file not found"}
|
||||
|
||||
try:
|
||||
# Read last N lines
|
||||
with open(log_file, 'r') as f:
|
||||
all_lines = f.readlines()
|
||||
log_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||
|
||||
return {
|
||||
"server_id": server_id,
|
||||
"server_name": server.name,
|
||||
"total_lines": len(all_lines),
|
||||
"returned_lines": len(log_lines),
|
||||
"lines": [line.rstrip() for line in log_lines]
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading log: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/logs/raw")
|
||||
async def get_server_logs_raw(
|
||||
server_id: int,
|
||||
lines: int = Query(default=100, le=5000),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get OpenVPN server log file as plain text."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
log_file = LOG_DIR / f"server-{server_id}.log"
|
||||
|
||||
if not log_file.exists():
|
||||
return Response(content="Log file not found", media_type="text/plain")
|
||||
|
||||
try:
|
||||
with open(log_file, 'r') as f:
|
||||
all_lines = f.readlines()
|
||||
log_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||
|
||||
return Response(
|
||||
content="".join(log_lines),
|
||||
media_type="text/plain"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading log: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/logs/supervisord")
|
||||
async def get_supervisord_logs(
|
||||
lines: int = Query(default=100, le=1000)
|
||||
):
|
||||
"""Get supervisord log file."""
|
||||
log_file = LOG_DIR / "supervisord.log"
|
||||
|
||||
if not log_file.exists():
|
||||
return {"lines": [], "message": "Supervisord log not found"}
|
||||
|
||||
try:
|
||||
with open(log_file, 'r') as f:
|
||||
all_lines = f.readlines()
|
||||
log_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||
|
||||
return {
|
||||
"total_lines": len(all_lines),
|
||||
"returned_lines": len(log_lines),
|
||||
"lines": [line.rstrip() for line in log_lines]
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading log: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/debug/sync")
|
||||
async def debug_sync(db: Session = Depends(get_db)):
|
||||
"""Debug endpoint to check VPN sync status."""
|
||||
sync_service = VPNSyncService(db)
|
||||
vpn_service = VPNServerService(db)
|
||||
|
||||
# Get all profiles with their CNs
|
||||
profiles = db.query(VPNProfile).all()
|
||||
profile_cns = [{"id": p.id, "name": p.name, "cert_cn": p.cert_cn, "gateway_id": p.gateway_id} for p in profiles]
|
||||
|
||||
# Get connected clients from all servers
|
||||
servers = db.query(VPNServer).filter(VPNServer.is_active == True).all()
|
||||
connected_clients = []
|
||||
|
||||
for server in servers:
|
||||
try:
|
||||
clients = vpn_service.get_connected_clients(server)
|
||||
for client in clients:
|
||||
client['server_id'] = server.id
|
||||
client['server_name'] = server.name
|
||||
connected_clients.append(client)
|
||||
except Exception as e:
|
||||
connected_clients.append({"server_id": server.id, "error": str(e)})
|
||||
|
||||
# Run sync and get result
|
||||
sync_result = sync_service.sync_all_connections()
|
||||
|
||||
return {
|
||||
"profiles": profile_cns,
|
||||
"connected_clients": connected_clients,
|
||||
"sync_result": sync_result
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Tenant management API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.tenant import Tenant
|
||||
from ..models.user import User
|
||||
from ..schemas.tenant import TenantCreate, TenantUpdate, TenantResponse
|
||||
from .deps import require_super_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=list[TenantResponse])
|
||||
def list_tenants(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin)
|
||||
):
|
||||
"""List all tenants (super admin only)."""
|
||||
tenants = db.query(Tenant).offset(skip).limit(limit).all()
|
||||
return tenants
|
||||
|
||||
|
||||
@router.post("/", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_tenant(
|
||||
tenant_data: TenantCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin)
|
||||
):
|
||||
"""Create a new tenant (super admin only)."""
|
||||
# Check if name already exists
|
||||
existing = db.query(Tenant).filter(Tenant.name == tenant_data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant with this name already exists"
|
||||
)
|
||||
|
||||
tenant = Tenant(**tenant_data.model_dump())
|
||||
db.add(tenant)
|
||||
db.commit()
|
||||
db.refresh(tenant)
|
||||
return tenant
|
||||
|
||||
|
||||
@router.get("/{tenant_id}", response_model=TenantResponse)
|
||||
def get_tenant(
|
||||
tenant_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin)
|
||||
):
|
||||
"""Get tenant by ID (super admin only)."""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
return tenant
|
||||
|
||||
|
||||
@router.put("/{tenant_id}", response_model=TenantResponse)
|
||||
def update_tenant(
|
||||
tenant_id: int,
|
||||
tenant_data: TenantUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin)
|
||||
):
|
||||
"""Update tenant (super admin only)."""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
update_data = tenant_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(tenant, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(tenant)
|
||||
return tenant
|
||||
|
||||
|
||||
@router.delete("/{tenant_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_tenant(
|
||||
tenant_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin)
|
||||
):
|
||||
"""Delete tenant (super admin only)."""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
db.delete(tenant)
|
||||
db.commit()
|
||||
@@ -0,0 +1,176 @@
|
||||
"""User management API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User, UserRole
|
||||
from ..schemas.user import UserCreate, UserUpdate, UserResponse
|
||||
from ..utils.security import get_password_hash
|
||||
from .deps import get_current_user, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=list[UserResponse])
|
||||
def list_users(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""List users (admins see their tenant's users, super admins see all)."""
|
||||
query = db.query(User)
|
||||
|
||||
# Filter by tenant for non-super-admins
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
query = query.filter(User.tenant_id == current_user.tenant_id)
|
||||
|
||||
users = query.offset(skip).limit(limit).all()
|
||||
return users
|
||||
|
||||
|
||||
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_user(
|
||||
user_data: UserCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Create a new user."""
|
||||
# Check permissions
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
# Regular admins can only create users in their own tenant
|
||||
if user_data.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot create users in other tenants"
|
||||
)
|
||||
# Cannot create super admins
|
||||
if user_data.role == UserRole.SUPER_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot create super admin users"
|
||||
)
|
||||
|
||||
# Check if username exists
|
||||
existing = db.query(User).filter(User.username == user_data.username).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already exists"
|
||||
)
|
||||
|
||||
# Check if email exists
|
||||
existing = db.query(User).filter(User.email == user_data.email).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already exists"
|
||||
)
|
||||
|
||||
user = User(
|
||||
username=user_data.username,
|
||||
email=user_data.email,
|
||||
password_hash=get_password_hash(user_data.password),
|
||||
full_name=user_data.full_name,
|
||||
role=user_data.role,
|
||||
tenant_id=user_data.tenant_id
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Get user by ID."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if user.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot access users from other tenants"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserResponse)
|
||||
def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Update user."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if user.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot modify users from other tenants"
|
||||
)
|
||||
|
||||
update_data = user_data.model_dump(exclude_unset=True)
|
||||
|
||||
# Hash password if provided
|
||||
if "password" in update_data:
|
||||
update_data["password_hash"] = get_password_hash(update_data.pop("password"))
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Delete user."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Cannot delete yourself
|
||||
if user.id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot delete your own account"
|
||||
)
|
||||
|
||||
# Check tenant access
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if user.tenant_id != current_user.tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot delete users from other tenants"
|
||||
)
|
||||
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user