batman
This commit is contained in:
commit
66f9ce3614
33 changed files with 2271 additions and 0 deletions
175
app/models/network.py
Normal file
175
app/models/network.py
Normal file
|
@ -0,0 +1,175 @@
|
|||
"""
|
||||
Network models for topology visualization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
import json
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class Network(db.Model):
|
||||
"""Network model representing a network topology."""
|
||||
__tablename__ = "networks"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
# Ownership
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
||||
owner = db.relationship("User", back_populates="networks")
|
||||
|
||||
# Related objects
|
||||
subnets = db.relationship("Subnet", back_populates="network", cascade="all, delete-orphan")
|
||||
devices = db.relationship("Device", back_populates="network", cascade="all, delete-orphan")
|
||||
firewall_rules = db.relationship("FirewallRule", back_populates="network", cascade="all, delete-orphan")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert network to dictionary for API responses and exports."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"subnets": [subnet.to_dict() for subnet in self.subnets],
|
||||
"devices": [device.to_dict() for device in self.devices],
|
||||
"firewall_rules": [rule.to_dict() for rule in self.firewall_rules]
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert network to JSON string for exports."""
|
||||
return json.dumps(self.to_dict(), indent=2)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any], user_id: int) -> "Network":
|
||||
"""Create a network from a dictionary (for imports)."""
|
||||
network = cls(
|
||||
name=data["name"],
|
||||
description=data.get("description", ""),
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
return network
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Network {self.name}>"
|
||||
|
||||
|
||||
class Subnet(db.Model):
|
||||
"""Subnet model representing a network subnet."""
|
||||
__tablename__ = "subnets"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False)
|
||||
cidr = db.Column(db.String(64), nullable=False)
|
||||
vlan = db.Column(db.Integer)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
# Parent network
|
||||
network_id = db.Column(db.Integer, db.ForeignKey("networks.id"), nullable=False)
|
||||
network = db.relationship("Network", back_populates="subnets")
|
||||
|
||||
# Devices in this subnet
|
||||
devices = db.relationship("Device", back_populates="subnet")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert subnet to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"cidr": self.cidr,
|
||||
"vlan": self.vlan,
|
||||
"description": self.description
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Subnet {self.cidr}>"
|
||||
|
||||
|
||||
class Device(db.Model):
|
||||
"""Device model representing a network device or host."""
|
||||
__tablename__ = "devices"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False)
|
||||
ip_address = db.Column(db.String(64))
|
||||
mac_address = db.Column(db.String(64))
|
||||
device_type = db.Column(db.String(64)) # server, router, switch, etc.
|
||||
os = db.Column(db.String(128))
|
||||
description = db.Column(db.Text)
|
||||
|
||||
# Parent network
|
||||
network_id = db.Column(db.Integer, db.ForeignKey("networks.id"), nullable=False)
|
||||
network = db.relationship("Network", back_populates="devices")
|
||||
|
||||
# Subnet membership
|
||||
subnet_id = db.Column(db.Integer, db.ForeignKey("subnets.id"))
|
||||
subnet = db.relationship("Subnet", back_populates="devices")
|
||||
|
||||
# Additional properties stored as JSON
|
||||
properties = db.Column(db.Text)
|
||||
|
||||
def get_properties(self) -> Dict[str, Any]:
|
||||
"""Get device properties from JSON field."""
|
||||
if not self.properties:
|
||||
return {}
|
||||
return json.loads(self.properties)
|
||||
|
||||
def set_properties(self, properties: Dict[str, Any]) -> None:
|
||||
"""Set device properties as JSON."""
|
||||
self.properties = json.dumps(properties)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert device to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"ip_address": self.ip_address,
|
||||
"mac_address": self.mac_address,
|
||||
"device_type": self.device_type,
|
||||
"os": self.os,
|
||||
"description": self.description,
|
||||
"subnet_id": self.subnet_id,
|
||||
"properties": self.get_properties()
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Device {self.name} ({self.ip_address})>"
|
||||
|
||||
|
||||
class FirewallRule(db.Model):
|
||||
"""Firewall rule model for documenting network security policies."""
|
||||
__tablename__ = "firewall_rules"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False)
|
||||
source = db.Column(db.String(128), nullable=False)
|
||||
destination = db.Column(db.String(128), nullable=False)
|
||||
protocol = db.Column(db.String(16)) # tcp, udp, icmp, any
|
||||
port_range = db.Column(db.String(64)) # e.g., "80,443" or "1024-2048"
|
||||
action = db.Column(db.String(16), nullable=False) # allow, deny
|
||||
description = db.Column(db.Text)
|
||||
|
||||
# Parent network
|
||||
network_id = db.Column(db.Integer, db.ForeignKey("networks.id"), nullable=False)
|
||||
network = db.relationship("Network", back_populates="firewall_rules")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert firewall rule to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"source": self.source,
|
||||
"destination": self.destination,
|
||||
"protocol": self.protocol,
|
||||
"port_range": self.port_range,
|
||||
"action": self.action,
|
||||
"description": self.description
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<FirewallRule {self.name}: {self.source} to {self.destination}>"
|
106
app/models/user.py
Normal file
106
app/models/user.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
"""
|
||||
User model for authentication and authorization.
|
||||
"""
|
||||
import bcrypt
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List
|
||||
import uuid
|
||||
|
||||
from flask_login import UserMixin
|
||||
|
||||
from app.extensions import db, login_manager
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
"""User model for authentication and authorization."""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
last_login = db.Column(db.DateTime)
|
||||
|
||||
# For account lockout/security
|
||||
failed_login_count = db.Column(db.Integer, default=0)
|
||||
locked_until = db.Column(db.DateTime)
|
||||
|
||||
# Password reset
|
||||
reset_token = db.Column(db.String(64), unique=True, index=True)
|
||||
reset_token_expires_at = db.Column(db.DateTime)
|
||||
|
||||
# Relationships
|
||||
networks = db.relationship("Network", back_populates="owner", cascade="all, delete-orphan")
|
||||
|
||||
def set_password(self, password: str) -> None:
|
||||
"""Hash and set the user's password."""
|
||||
# Generate a salt and hash the password
|
||||
self.password_hash = bcrypt.hashpw(
|
||||
password.encode("utf-8"), bcrypt.gensalt()
|
||||
).decode("utf-8")
|
||||
|
||||
def check_password(self, password: str) -> bool:
|
||||
"""Verify the provided password against the stored hash."""
|
||||
return bcrypt.checkpw(
|
||||
password.encode("utf-8"), self.password_hash.encode("utf-8")
|
||||
)
|
||||
|
||||
def generate_reset_token(self, expires_in: int = 3600) -> str:
|
||||
"""Generate a password reset token that expires after the specified time."""
|
||||
self.reset_token = str(uuid.uuid4())
|
||||
self.reset_token_expires_at = datetime.utcnow() + timedelta(seconds=expires_in)
|
||||
db.session.commit()
|
||||
return self.reset_token
|
||||
|
||||
def clear_reset_token(self) -> None:
|
||||
"""Clear the reset token after it's been used."""
|
||||
self.reset_token = None
|
||||
self.reset_token_expires_at = None
|
||||
db.session.commit()
|
||||
|
||||
def is_reset_token_valid(self, token: str) -> bool:
|
||||
"""Check if the provided reset token is valid and not expired."""
|
||||
return (
|
||||
self.reset_token == token
|
||||
and self.reset_token_expires_at is not None
|
||||
and self.reset_token_expires_at > datetime.utcnow()
|
||||
)
|
||||
|
||||
def record_login(self, success: bool) -> None:
|
||||
"""Record a login attempt."""
|
||||
if success:
|
||||
self.last_login = datetime.utcnow()
|
||||
self.failed_login_count = 0
|
||||
self.locked_until = None
|
||||
else:
|
||||
self.failed_login_count += 1
|
||||
if self.failed_login_count >= 5: # Threshold from config
|
||||
self.locked_until = datetime.utcnow() + timedelta(minutes=30) # Duration from config
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def is_account_locked(self) -> bool:
|
||||
"""Check if the account is locked due to too many failed login attempts."""
|
||||
if self.locked_until is None:
|
||||
return False
|
||||
|
||||
if datetime.utcnow() > self.locked_until:
|
||||
self.failed_login_count = 0
|
||||
self.locked_until = None
|
||||
db.session.commit()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User {self.username}>"
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id: str) -> Optional[User]:
|
||||
"""Load a user from the database by ID."""
|
||||
return User.query.get(int(user_id))
|
Loading…
Add table
Add a link
Reference in a new issue