first commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"""SQLAlchemy models for mGuard VPN Endpoint Server."""
|
||||
|
||||
from .tenant import Tenant
|
||||
from .user import User
|
||||
from .gateway import Gateway, RouterType
|
||||
from .endpoint import Endpoint, ApplicationTemplate
|
||||
from .access import UserGatewayAccess, UserEndpointAccess, ConnectionLog
|
||||
from .certificate_authority import CertificateAuthority, CAStatus, CAAlgorithm
|
||||
from .vpn_server import VPNServer, VPNProtocol, VPNCipher, VPNAuth, VPNCompression, VPNServerStatus
|
||||
from .vpn_profile import VPNProfile, VPNProfileStatus
|
||||
from .vpn_connection_log import VPNConnectionLog
|
||||
|
||||
__all__ = [
|
||||
"Tenant",
|
||||
"User",
|
||||
"Gateway",
|
||||
"RouterType",
|
||||
"Endpoint",
|
||||
"ApplicationTemplate",
|
||||
"UserGatewayAccess",
|
||||
"UserEndpointAccess",
|
||||
"ConnectionLog",
|
||||
# PKI & VPN
|
||||
"CertificateAuthority",
|
||||
"CAStatus",
|
||||
"CAAlgorithm",
|
||||
"VPNServer",
|
||||
"VPNProtocol",
|
||||
"VPNCipher",
|
||||
"VPNAuth",
|
||||
"VPNCompression",
|
||||
"VPNServerStatus",
|
||||
"VPNProfile",
|
||||
"VPNProfileStatus",
|
||||
"VPNConnectionLog",
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Access control and logging models."""
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class UserGatewayAccess(Base):
|
||||
"""User access to gateways."""
|
||||
|
||||
__tablename__ = "user_gateway_access"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
|
||||
granted_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
granted_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", foreign_keys=[user_id], back_populates="gateway_access")
|
||||
gateway = relationship("Gateway", back_populates="user_access")
|
||||
granted_by = relationship("User", foreign_keys=[granted_by_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserGatewayAccess(user_id={self.user_id}, gateway_id={self.gateway_id})>"
|
||||
|
||||
|
||||
class UserEndpointAccess(Base):
|
||||
"""User access to specific endpoints (optional fine-grained control)."""
|
||||
|
||||
__tablename__ = "user_endpoint_access"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
endpoint_id = Column(Integer, ForeignKey("endpoints.id"), nullable=False)
|
||||
granted_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
granted_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", foreign_keys=[user_id], back_populates="endpoint_access")
|
||||
endpoint = relationship("Endpoint", back_populates="user_access")
|
||||
granted_by = relationship("User", foreign_keys=[granted_by_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserEndpointAccess(user_id={self.user_id}, endpoint_id={self.endpoint_id})>"
|
||||
|
||||
|
||||
class ConnectionLog(Base):
|
||||
"""Log of VPN connections for auditing."""
|
||||
|
||||
__tablename__ = "connection_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
|
||||
endpoint_id = Column(Integer, ForeignKey("endpoints.id"), nullable=True)
|
||||
|
||||
# Connection details
|
||||
client_ip = Column(String(45), nullable=True) # Client's real IP
|
||||
vpn_ip = Column(String(45), nullable=True) # Assigned VPN IP
|
||||
connected_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
disconnected_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="connection_logs")
|
||||
gateway = relationship("Gateway", back_populates="connection_logs")
|
||||
endpoint = relationship("Endpoint", back_populates="connection_logs")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ConnectionLog(user={self.user_id}, gateway={self.gateway_id}, connected={self.connected_at})>"
|
||||
|
||||
@property
|
||||
def duration_seconds(self) -> int | None:
|
||||
"""Get connection duration in seconds."""
|
||||
if self.disconnected_at:
|
||||
return int((self.disconnected_at - self.connected_at).total_seconds())
|
||||
return None
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Certificate Authority model for PKI management."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class CAStatus(str, PyEnum):
|
||||
"""Certificate Authority status."""
|
||||
PENDING = "pending" # DH parameters being generated
|
||||
ACTIVE = "active"
|
||||
EXPIRED = "expired"
|
||||
REVOKED = "revoked"
|
||||
|
||||
|
||||
class CAAlgorithm(str, PyEnum):
|
||||
"""Key algorithm for CA."""
|
||||
RSA = "rsa"
|
||||
ECDSA = "ecdsa"
|
||||
|
||||
|
||||
class CertificateAuthority(Base):
|
||||
"""Certificate Authority for issuing VPN certificates."""
|
||||
|
||||
__tablename__ = "certificate_authorities"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True) # NULL for global CA
|
||||
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Certificate data (PEM encoded)
|
||||
ca_cert = Column(Text, nullable=True) # CA certificate
|
||||
ca_key = Column(Text, nullable=True) # CA private key (encrypted)
|
||||
|
||||
# DH parameters (shared across servers using this CA)
|
||||
dh_params = Column(Text, nullable=True) # Pre-generated DH parameters
|
||||
dh_generating = Column(Boolean, default=False) # DH generation in progress
|
||||
|
||||
# Key configuration
|
||||
key_size = Column(Integer, default=4096)
|
||||
algorithm = Column(Enum(CAAlgorithm), default=CAAlgorithm.RSA)
|
||||
|
||||
# Validity
|
||||
valid_from = Column(DateTime, nullable=True)
|
||||
valid_until = Column(DateTime, nullable=True)
|
||||
|
||||
# Status
|
||||
is_default = Column(Boolean, default=False) # Default CA for new certificates
|
||||
status = Column(Enum(CAStatus), default=CAStatus.PENDING)
|
||||
|
||||
# CRL (Certificate Revocation List)
|
||||
crl = Column(Text, nullable=True)
|
||||
crl_updated_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Serial number tracking
|
||||
next_serial = Column(Integer, default=1)
|
||||
|
||||
# Audit
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
tenant = relationship("Tenant", back_populates="certificate_authorities")
|
||||
created_by = relationship("User", foreign_keys=[created_by_id])
|
||||
vpn_servers = relationship("VPNServer", back_populates="certificate_authority")
|
||||
vpn_profiles = relationship("VPNProfile", back_populates="certificate_authority")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CertificateAuthority(id={self.id}, name='{self.name}', status='{self.status}')>"
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""Check if CA is ready for issuing certificates."""
|
||||
return (
|
||||
self.status == CAStatus.ACTIVE and
|
||||
self.ca_cert is not None and
|
||||
self.ca_key is not None and
|
||||
self.dh_params is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if CA certificate is expired."""
|
||||
if self.valid_until:
|
||||
return datetime.utcnow() > self.valid_until
|
||||
return False
|
||||
|
||||
@property
|
||||
def days_until_expiry(self) -> int | None:
|
||||
"""Days until CA expires."""
|
||||
if self.valid_until:
|
||||
delta = self.valid_until - datetime.utcnow()
|
||||
return max(0, delta.days)
|
||||
return None
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Endpoint model for devices behind gateways."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Protocol(str, PyEnum):
|
||||
"""Network protocol for endpoint access."""
|
||||
TCP = "tcp"
|
||||
UDP = "udp"
|
||||
|
||||
|
||||
class Endpoint(Base):
|
||||
"""Endpoint model representing a device/service behind a gateway."""
|
||||
|
||||
__tablename__ = "endpoints"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
|
||||
|
||||
# Endpoint info
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Network configuration
|
||||
internal_ip = Column(String(45), nullable=False) # IP in customer network
|
||||
port = Column(Integer, nullable=False)
|
||||
protocol = Column(Enum(Protocol), default=Protocol.TCP, nullable=False)
|
||||
|
||||
# Application info
|
||||
application_name = Column(String(100), nullable=True) # e.g., "CoDeSys", "SSH", "HTTP"
|
||||
application_template_id = Column(Integer, ForeignKey("application_templates.id"), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
gateway = relationship("Gateway", back_populates="endpoints")
|
||||
user_access = relationship("UserEndpointAccess", back_populates="endpoint", cascade="all, delete-orphan")
|
||||
application_template = relationship("ApplicationTemplate")
|
||||
connection_logs = relationship("ConnectionLog", back_populates="endpoint")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Endpoint(id={self.id}, name='{self.name}', ip='{self.internal_ip}:{self.port}')>"
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
"""Get full address string."""
|
||||
return f"{self.internal_ip}:{self.port}"
|
||||
|
||||
|
||||
class ApplicationTemplate(Base):
|
||||
"""Pre-defined application templates with default ports."""
|
||||
|
||||
__tablename__ = "application_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), nullable=False, unique=True)
|
||||
description = Column(Text, nullable=True)
|
||||
default_port = Column(Integer, nullable=False)
|
||||
protocol = Column(Enum(Protocol), default=Protocol.TCP, nullable=False)
|
||||
icon = Column(String(100), nullable=True) # Icon name for client UI
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ApplicationTemplate(name='{self.name}', port={self.default_port})>"
|
||||
|
||||
|
||||
# Default application templates to be seeded
|
||||
DEFAULT_APPLICATION_TEMPLATES = [
|
||||
{"name": "CoDeSys", "description": "CoDeSys Runtime/Gateway", "default_port": 11740, "protocol": "tcp"},
|
||||
{"name": "CoDeSys Gateway", "description": "CoDeSys Gateway Service", "default_port": 1217, "protocol": "tcp"},
|
||||
{"name": "SSH", "description": "Secure Shell", "default_port": 22, "protocol": "tcp"},
|
||||
{"name": "HTTP", "description": "Web Interface", "default_port": 80, "protocol": "tcp"},
|
||||
{"name": "HTTPS", "description": "Secure Web Interface", "default_port": 443, "protocol": "tcp"},
|
||||
{"name": "VNC", "description": "Virtual Network Computing", "default_port": 5900, "protocol": "tcp"},
|
||||
{"name": "RDP", "description": "Remote Desktop Protocol", "default_port": 3389, "protocol": "tcp"},
|
||||
{"name": "Modbus TCP", "description": "Modbus over TCP/IP", "default_port": 502, "protocol": "tcp"},
|
||||
{"name": "OPC UA", "description": "OPC Unified Architecture", "default_port": 4840, "protocol": "tcp"},
|
||||
{"name": "MQTT", "description": "Message Queue Telemetry Transport", "default_port": 1883, "protocol": "tcp"},
|
||||
{"name": "S7 Communication", "description": "Siemens S7 Protocol", "default_port": 102, "protocol": "tcp"},
|
||||
]
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Gateway model for mGuard routers."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class RouterType(str, PyEnum):
|
||||
"""Supported mGuard router types."""
|
||||
FL_MGUARD_2000 = "FL_MGUARD_2000"
|
||||
FL_MGUARD_4000 = "FL_MGUARD_4000"
|
||||
FL_MGUARD_RS4000 = "FL_MGUARD_RS4000"
|
||||
FL_MGUARD_1000 = "FL_MGUARD_1000"
|
||||
|
||||
|
||||
class ProvisioningMethod(str, PyEnum):
|
||||
"""Method used to provision the gateway."""
|
||||
REST_API = "rest_api" # For firmware 10.5.x+
|
||||
SSH = "ssh" # For older firmware
|
||||
ATV_FILE = "atv_file" # Offline via config file
|
||||
|
||||
|
||||
class Gateway(Base):
|
||||
"""Gateway model representing an mGuard router."""
|
||||
|
||||
__tablename__ = "gateways"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=False)
|
||||
|
||||
# Basic info
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
location = Column(String(255), nullable=True) # Physical location
|
||||
|
||||
# Router details
|
||||
serial_number = Column(String(100), nullable=True, unique=True)
|
||||
router_type = Column(Enum(RouterType), nullable=False)
|
||||
firmware_version = Column(String(50), nullable=True)
|
||||
provisioning_method = Column(Enum(ProvisioningMethod), default=ProvisioningMethod.ATV_FILE)
|
||||
|
||||
# VPN configuration
|
||||
vpn_ip = Column(String(45), nullable=True, unique=True) # IPv4/IPv6
|
||||
vpn_cert_cn = Column(String(255), nullable=True, unique=True) # Certificate Common Name
|
||||
vpn_subnet = Column(String(50), nullable=True) # Network behind gateway, e.g., "10.0.0.0/24"
|
||||
|
||||
# Status
|
||||
is_online = Column(Boolean, default=False, nullable=False)
|
||||
is_provisioned = Column(Boolean, default=False, nullable=False)
|
||||
last_seen = Column(DateTime, nullable=True)
|
||||
last_config_update = Column(DateTime, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant = relationship("Tenant", back_populates="gateways")
|
||||
endpoints = relationship("Endpoint", back_populates="gateway", cascade="all, delete-orphan")
|
||||
user_access = relationship("UserGatewayAccess", back_populates="gateway", cascade="all, delete-orphan")
|
||||
connection_logs = relationship("ConnectionLog", back_populates="gateway")
|
||||
vpn_profiles = relationship("VPNProfile", back_populates="gateway", cascade="all, delete-orphan", order_by="VPNProfile.priority")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Gateway(id={self.id}, name='{self.name}', type='{self.router_type}')>"
|
||||
|
||||
@property
|
||||
def supports_rest_api(self) -> bool:
|
||||
"""Check if gateway supports REST API (firmware 10.x+)."""
|
||||
if not self.firmware_version:
|
||||
return False
|
||||
try:
|
||||
major_version = int(self.firmware_version.split('.')[0])
|
||||
return major_version >= 10
|
||||
except (ValueError, IndexError):
|
||||
return False
|
||||
|
||||
@property
|
||||
def primary_profile(self):
|
||||
"""Get the primary VPN profile (highest priority)."""
|
||||
active_profiles = [p for p in self.vpn_profiles if p.is_active]
|
||||
if active_profiles:
|
||||
return min(active_profiles, key=lambda p: p.priority)
|
||||
return None
|
||||
|
||||
@property
|
||||
def has_vpn_profiles(self) -> bool:
|
||||
"""Check if gateway has any VPN profiles."""
|
||||
return len(self.vpn_profiles) > 0
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Tenant model for multi-tenant support."""
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Tenant(Base):
|
||||
"""Tenant/Customer model for multi-tenant separation."""
|
||||
|
||||
__tablename__ = "tenants"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(255), nullable=False, unique=True)
|
||||
description = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
users = relationship("User", back_populates="tenant", cascade="all, delete-orphan")
|
||||
gateways = relationship("Gateway", back_populates="tenant", cascade="all, delete-orphan")
|
||||
certificate_authorities = relationship("CertificateAuthority", back_populates="tenant", cascade="all, delete-orphan")
|
||||
vpn_servers = relationship("VPNServer", back_populates="tenant", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tenant(id={self.id}, name='{self.name}')>"
|
||||
@@ -0,0 +1,62 @@
|
||||
"""User model with role-based access control."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class UserRole(str, PyEnum):
|
||||
"""User roles for access control."""
|
||||
SUPER_ADMIN = "super_admin" # Can manage all tenants
|
||||
ADMIN = "admin" # Can manage own tenant
|
||||
TECHNICIAN = "technician" # Can connect to assigned gateways
|
||||
VIEWER = "viewer" # Read-only access
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model with multi-tenant support."""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True) # NULL for super_admin
|
||||
username = Column(String(255), nullable=False, unique=True, index=True)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
email = Column(String(255), nullable=False, unique=True)
|
||||
full_name = Column(String(255), nullable=True)
|
||||
role = Column(Enum(UserRole), default=UserRole.TECHNICIAN, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant = relationship("Tenant", back_populates="users")
|
||||
gateway_access = relationship(
|
||||
"UserGatewayAccess",
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
primaryjoin="User.id == UserGatewayAccess.user_id"
|
||||
)
|
||||
endpoint_access = relationship(
|
||||
"UserEndpointAccess",
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
primaryjoin="User.id == UserEndpointAccess.user_id"
|
||||
)
|
||||
connection_logs = relationship("ConnectionLog", back_populates="user")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(id={self.id}, username='{self.username}', role='{self.role}')>"
|
||||
|
||||
@property
|
||||
def is_super_admin(self) -> bool:
|
||||
"""Check if user is super admin."""
|
||||
return self.role == UserRole.SUPER_ADMIN
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user has admin privileges."""
|
||||
return self.role in (UserRole.SUPER_ADMIN, UserRole.ADMIN)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""VPN Connection Log model for tracking profile connection history."""
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, BigInteger
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class VPNConnectionLog(Base):
|
||||
"""Log of VPN profile connections."""
|
||||
|
||||
__tablename__ = "vpn_connection_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vpn_profile_id = Column(Integer, ForeignKey("vpn_profiles.id"), nullable=False)
|
||||
vpn_server_id = Column(Integer, ForeignKey("vpn_servers.id"), nullable=False)
|
||||
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
|
||||
|
||||
# Connection info
|
||||
common_name = Column(String(255), nullable=False)
|
||||
real_address = Column(String(255), nullable=True) # IP:Port
|
||||
vpn_ip = Column(String(15), nullable=True) # Assigned VPN IP
|
||||
|
||||
# Timestamps
|
||||
connected_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
disconnected_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Traffic stats (updated on disconnect)
|
||||
bytes_received = Column(BigInteger, default=0)
|
||||
bytes_sent = Column(BigInteger, default=0)
|
||||
|
||||
# Relationships
|
||||
vpn_profile = relationship("VPNProfile")
|
||||
vpn_server = relationship("VPNServer")
|
||||
gateway = relationship("Gateway")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VPNConnectionLog(id={self.id}, cn='{self.common_name}', connected={self.connected_at})>"
|
||||
|
||||
@property
|
||||
def duration_seconds(self) -> int | None:
|
||||
"""Connection duration in seconds."""
|
||||
if self.disconnected_at:
|
||||
return int((self.disconnected_at - self.connected_at).total_seconds())
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""Check if connection is still active."""
|
||||
return self.disconnected_at is None
|
||||
@@ -0,0 +1,90 @@
|
||||
"""VPN Profile model for gateway VPN configurations."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class VPNProfileStatus(str, PyEnum):
|
||||
"""VPN Profile status."""
|
||||
PENDING = "pending" # Certificate being generated
|
||||
ACTIVE = "active" # Ready to use
|
||||
PROVISIONED = "provisioned" # Downloaded/deployed to gateway
|
||||
EXPIRED = "expired" # Certificate expired
|
||||
REVOKED = "revoked" # Certificate revoked
|
||||
|
||||
|
||||
class VPNProfile(Base):
|
||||
"""VPN Profile for a gateway - links gateway to VPN server."""
|
||||
|
||||
__tablename__ = "vpn_profiles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
gateway_id = Column(Integer, ForeignKey("gateways.id"), nullable=False)
|
||||
vpn_server_id = Column(Integer, ForeignKey("vpn_servers.id"), nullable=False)
|
||||
ca_id = Column(Integer, ForeignKey("certificate_authorities.id"), nullable=False)
|
||||
|
||||
# Profile info
|
||||
name = Column(String(255), nullable=False) # e.g., "Produktion", "Fallback"
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Certificate data
|
||||
cert_cn = Column(String(255), nullable=False) # Common Name
|
||||
client_cert = Column(Text, nullable=True) # Client certificate PEM
|
||||
client_key = Column(Text, nullable=True) # Client private key PEM
|
||||
|
||||
# Priority for failover (1 = highest priority)
|
||||
priority = Column(Integer, default=1)
|
||||
|
||||
# Status
|
||||
status = Column(Enum(VPNProfileStatus), default=VPNProfileStatus.PENDING)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Validity
|
||||
valid_from = Column(DateTime, nullable=True)
|
||||
valid_until = Column(DateTime, nullable=True)
|
||||
|
||||
# Provisioning tracking
|
||||
provisioned_at = Column(DateTime, nullable=True)
|
||||
last_connection = Column(DateTime, nullable=True)
|
||||
|
||||
# VPN IP assigned to this profile (if static)
|
||||
vpn_ip = Column(String(15), nullable=True)
|
||||
|
||||
# Audit
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
gateway = relationship("Gateway", back_populates="vpn_profiles")
|
||||
vpn_server = relationship("VPNServer", back_populates="vpn_profiles")
|
||||
certificate_authority = relationship("CertificateAuthority", back_populates="vpn_profiles")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VPNProfile(id={self.id}, name='{self.name}', gateway_id={self.gateway_id}, priority={self.priority})>"
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""Check if profile is ready for provisioning."""
|
||||
return (
|
||||
self.status in (VPNProfileStatus.ACTIVE, VPNProfileStatus.PROVISIONED) and
|
||||
self.client_cert is not None and
|
||||
self.client_key is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if certificate is expired."""
|
||||
if self.valid_until:
|
||||
return datetime.utcnow() > self.valid_until
|
||||
return False
|
||||
|
||||
@property
|
||||
def days_until_expiry(self) -> int | None:
|
||||
"""Days until certificate expires."""
|
||||
if self.valid_until:
|
||||
delta = self.valid_until - datetime.utcnow()
|
||||
return max(0, delta.days)
|
||||
return None
|
||||
@@ -0,0 +1,130 @@
|
||||
"""VPN Server model for managing multiple OpenVPN instances."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class VPNProtocol(str, PyEnum):
|
||||
"""VPN protocol type."""
|
||||
UDP = "udp"
|
||||
TCP = "tcp"
|
||||
|
||||
|
||||
class VPNCipher(str, PyEnum):
|
||||
"""VPN cipher options."""
|
||||
AES_256_GCM = "AES-256-GCM"
|
||||
AES_128_GCM = "AES-128-GCM"
|
||||
AES_256_CBC = "AES-256-CBC"
|
||||
CHACHA20_POLY1305 = "CHACHA20-POLY1305"
|
||||
|
||||
|
||||
class VPNAuth(str, PyEnum):
|
||||
"""VPN auth digest options."""
|
||||
SHA256 = "SHA256"
|
||||
SHA384 = "SHA384"
|
||||
SHA512 = "SHA512"
|
||||
|
||||
|
||||
class VPNCompression(str, PyEnum):
|
||||
"""VPN compression options."""
|
||||
NONE = "none"
|
||||
LZ4 = "lz4"
|
||||
LZ4_V2 = "lz4-v2"
|
||||
LZO = "lzo"
|
||||
|
||||
|
||||
class VPNServerStatus(str, PyEnum):
|
||||
"""VPN Server status."""
|
||||
PENDING = "pending" # Server created but not started
|
||||
STARTING = "starting" # Container starting
|
||||
RUNNING = "running" # Server running
|
||||
STOPPED = "stopped" # Server stopped
|
||||
ERROR = "error" # Error state
|
||||
|
||||
|
||||
class VPNServer(Base):
|
||||
"""VPN Server instance configuration."""
|
||||
|
||||
__tablename__ = "vpn_servers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True) # NULL for global
|
||||
ca_id = Column(Integer, ForeignKey("certificate_authorities.id"), nullable=False)
|
||||
|
||||
# Basic info
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Network configuration
|
||||
hostname = Column(String(255), nullable=False) # External hostname/IP
|
||||
port = Column(Integer, default=1194, nullable=False)
|
||||
protocol = Column(Enum(VPNProtocol), default=VPNProtocol.UDP, nullable=False)
|
||||
|
||||
# VPN network
|
||||
vpn_network = Column(String(18), default="10.8.0.0", nullable=False) # CIDR or IP
|
||||
vpn_netmask = Column(String(15), default="255.255.255.0", nullable=False)
|
||||
|
||||
# Server certificate (PEM encoded)
|
||||
server_cert = Column(Text, nullable=True)
|
||||
server_key = Column(Text, nullable=True)
|
||||
ta_key = Column(Text, nullable=True) # TLS-Auth key
|
||||
|
||||
# Security settings
|
||||
cipher = Column(Enum(VPNCipher), default=VPNCipher.AES_256_GCM)
|
||||
auth = Column(Enum(VPNAuth), default=VPNAuth.SHA256)
|
||||
tls_version_min = Column(String(10), default="1.2")
|
||||
tls_auth_enabled = Column(Boolean, default=True)
|
||||
|
||||
# Performance settings
|
||||
compression = Column(Enum(VPNCompression), default=VPNCompression.NONE)
|
||||
max_clients = Column(Integer, default=100)
|
||||
keepalive_interval = Column(Integer, default=10) # seconds
|
||||
keepalive_timeout = Column(Integer, default=60) # seconds
|
||||
|
||||
# Docker settings
|
||||
docker_container_name = Column(String(255), nullable=True)
|
||||
management_port = Column(Integer, default=7505)
|
||||
|
||||
# Status
|
||||
status = Column(Enum(VPNServerStatus), default=VPNServerStatus.PENDING)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_primary = Column(Boolean, default=False) # Primary server for tenant
|
||||
auto_start = Column(Boolean, default=True) # Start on system boot
|
||||
|
||||
# Statistics (updated periodically)
|
||||
connected_clients = Column(Integer, default=0)
|
||||
last_status_check = Column(DateTime, nullable=True)
|
||||
|
||||
# Audit
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tenant = relationship("Tenant", back_populates="vpn_servers")
|
||||
certificate_authority = relationship("CertificateAuthority", back_populates="vpn_servers")
|
||||
vpn_profiles = relationship("VPNProfile", back_populates="vpn_server")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VPNServer(id={self.id}, name='{self.name}', {self.hostname}:{self.port}/{self.protocol.value})>"
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
"""Check if server is ready to accept connections."""
|
||||
return (
|
||||
self.server_cert is not None and
|
||||
self.server_key is not None and
|
||||
self.certificate_authority is not None and
|
||||
self.certificate_authority.is_ready
|
||||
)
|
||||
|
||||
@property
|
||||
def connection_string(self) -> str:
|
||||
"""Get connection string for display."""
|
||||
return f"{self.hostname}:{self.port}/{self.protocol.value.upper()}"
|
||||
|
||||
def get_docker_port_mapping(self) -> dict:
|
||||
"""Get Docker port mapping for this server."""
|
||||
return {f"{self.port}/{self.protocol.value}": self.port}
|
||||
Reference in New Issue
Block a user