feat: add hosts and params files, fix rules SECTION NEW header
All checks were successful
Build containers when image tags change / build-if-image-changed (backend, shorefront-backend, shorefront backend, backend/Dockerfile, git.baumann.gr/adebaumann/shorefront-backend, .backend.image) (push) Successful in 44s
Build containers when image tags change / build-if-image-changed (frontend, shorefront-frontend, shorefront frontend, frontend/Dockerfile, git.baumann.gr/adebaumann/shorefront-frontend, .frontend.image) (push) Successful in 1m32s
All checks were successful
Build containers when image tags change / build-if-image-changed (backend, shorefront-backend, shorefront backend, backend/Dockerfile, git.baumann.gr/adebaumann/shorefront-backend, .backend.image) (push) Successful in 44s
Build containers when image tags change / build-if-image-changed (frontend, shorefront-frontend, shorefront frontend, frontend/Dockerfile, git.baumann.gr/adebaumann/shorefront-frontend, .frontend.image) (push) Successful in 1m32s
This commit is contained in:
37
backend/alembic/versions/0004_add_hosts_and_params.py
Normal file
37
backend/alembic/versions/0004_add_hosts_and_params.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""add hosts and params tables
|
||||
|
||||
Revision ID: 0004
|
||||
Revises: 0003
|
||||
Create Date: 2026-03-01
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0004"
|
||||
down_revision = "0003"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"hosts",
|
||||
sa.Column("id", sa.Integer, primary_key=True),
|
||||
sa.Column("config_id", sa.Integer, sa.ForeignKey("configs.id"), nullable=False),
|
||||
sa.Column("zone_id", sa.Integer, sa.ForeignKey("zones.id"), nullable=False),
|
||||
sa.Column("interface", sa.String(32), nullable=False),
|
||||
sa.Column("subnet", sa.String(64), nullable=False),
|
||||
sa.Column("options", sa.Text, server_default="''"),
|
||||
)
|
||||
op.create_table(
|
||||
"params",
|
||||
sa.Column("id", sa.Integer, primary_key=True),
|
||||
sa.Column("config_id", sa.Integer, sa.ForeignKey("configs.id"), nullable=False),
|
||||
sa.Column("name", sa.String(64), nullable=False),
|
||||
sa.Column("value", sa.String(255), nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("params")
|
||||
op.drop_table("hosts")
|
||||
@@ -93,6 +93,8 @@ def generate_config(
|
||||
selectinload(models.Config.rules).selectinload(models.Rule.src_zone),
|
||||
selectinload(models.Config.rules).selectinload(models.Rule.dst_zone),
|
||||
selectinload(models.Config.snat_entries),
|
||||
selectinload(models.Config.host_entries).selectinload(models.Host.zone),
|
||||
selectinload(models.Config.params),
|
||||
)
|
||||
.filter(models.Config.id == config_id, models.Config.owner_id == current_user.id)
|
||||
.first()
|
||||
|
||||
64
backend/app/api/hosts.py
Normal file
64
backend/app/api/hosts.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app import models, schemas
|
||||
from app.auth import get_current_user
|
||||
from app.database import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _owner_config(config_id: int, db: Session, user: models.User) -> models.Config:
|
||||
config = db.query(models.Config).filter(
|
||||
models.Config.id == config_id, models.Config.owner_id == user.id
|
||||
).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
return config
|
||||
|
||||
|
||||
@router.get("/{config_id}/hosts", response_model=list[schemas.HostOut])
|
||||
def list_hosts(config_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
return db.query(models.Host).filter(models.Host.config_id == config_id).all()
|
||||
|
||||
|
||||
@router.post("/{config_id}/hosts", response_model=schemas.HostOut, status_code=201)
|
||||
def create_host(config_id: int, body: schemas.HostCreate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
host = models.Host(**body.model_dump(), config_id=config_id)
|
||||
db.add(host)
|
||||
db.commit()
|
||||
db.refresh(host)
|
||||
return host
|
||||
|
||||
|
||||
@router.get("/{config_id}/hosts/{host_id}", response_model=schemas.HostOut)
|
||||
def get_host(config_id: int, host_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
host = db.query(models.Host).filter(models.Host.id == host_id, models.Host.config_id == config_id).first()
|
||||
if not host:
|
||||
raise HTTPException(status_code=404, detail="Host entry not found")
|
||||
return host
|
||||
|
||||
|
||||
@router.put("/{config_id}/hosts/{host_id}", response_model=schemas.HostOut)
|
||||
def update_host(config_id: int, host_id: int, body: schemas.HostUpdate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
host = db.query(models.Host).filter(models.Host.id == host_id, models.Host.config_id == config_id).first()
|
||||
if not host:
|
||||
raise HTTPException(status_code=404, detail="Host entry not found")
|
||||
for field, value in body.model_dump(exclude_none=True).items():
|
||||
setattr(host, field, value)
|
||||
db.commit()
|
||||
db.refresh(host)
|
||||
return host
|
||||
|
||||
|
||||
@router.delete("/{config_id}/hosts/{host_id}", status_code=204)
|
||||
def delete_host(config_id: int, host_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
host = db.query(models.Host).filter(models.Host.id == host_id, models.Host.config_id == config_id).first()
|
||||
if not host:
|
||||
raise HTTPException(status_code=404, detail="Host entry not found")
|
||||
db.delete(host)
|
||||
db.commit()
|
||||
64
backend/app/api/params.py
Normal file
64
backend/app/api/params.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app import models, schemas
|
||||
from app.auth import get_current_user
|
||||
from app.database import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _owner_config(config_id: int, db: Session, user: models.User) -> models.Config:
|
||||
config = db.query(models.Config).filter(
|
||||
models.Config.id == config_id, models.Config.owner_id == user.id
|
||||
).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Config not found")
|
||||
return config
|
||||
|
||||
|
||||
@router.get("/{config_id}/params", response_model=list[schemas.ParamOut])
|
||||
def list_params(config_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
return db.query(models.Param).filter(models.Param.config_id == config_id).all()
|
||||
|
||||
|
||||
@router.post("/{config_id}/params", response_model=schemas.ParamOut, status_code=201)
|
||||
def create_param(config_id: int, body: schemas.ParamCreate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
param = models.Param(**body.model_dump(), config_id=config_id)
|
||||
db.add(param)
|
||||
db.commit()
|
||||
db.refresh(param)
|
||||
return param
|
||||
|
||||
|
||||
@router.get("/{config_id}/params/{param_id}", response_model=schemas.ParamOut)
|
||||
def get_param(config_id: int, param_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
param = db.query(models.Param).filter(models.Param.id == param_id, models.Param.config_id == config_id).first()
|
||||
if not param:
|
||||
raise HTTPException(status_code=404, detail="Param not found")
|
||||
return param
|
||||
|
||||
|
||||
@router.put("/{config_id}/params/{param_id}", response_model=schemas.ParamOut)
|
||||
def update_param(config_id: int, param_id: int, body: schemas.ParamUpdate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
param = db.query(models.Param).filter(models.Param.id == param_id, models.Param.config_id == config_id).first()
|
||||
if not param:
|
||||
raise HTTPException(status_code=404, detail="Param not found")
|
||||
for field, value in body.model_dump(exclude_none=True).items():
|
||||
setattr(param, field, value)
|
||||
db.commit()
|
||||
db.refresh(param)
|
||||
return param
|
||||
|
||||
|
||||
@router.delete("/{config_id}/params/{param_id}", status_code=204)
|
||||
def delete_param(config_id: int, param_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
|
||||
_owner_config(config_id, db, user)
|
||||
param = db.query(models.Param).filter(models.Param.id == param_id, models.Param.config_id == config_id).first()
|
||||
if not param:
|
||||
raise HTTPException(status_code=404, detail="Param not found")
|
||||
db.delete(param)
|
||||
db.commit()
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from app.api import auth, configs, zones, interfaces, policies, rules, snat
|
||||
from app.api import auth, configs, zones, interfaces, policies, rules, snat, hosts, params
|
||||
from app.database import settings
|
||||
|
||||
app = FastAPI(title="Shorefront", version="0.1.0")
|
||||
@@ -22,6 +22,8 @@ app.include_router(interfaces.router, prefix="/configs", tags=["interfaces"])
|
||||
app.include_router(policies.router, prefix="/configs", tags=["policies"])
|
||||
app.include_router(rules.router, prefix="/configs", tags=["rules"])
|
||||
app.include_router(snat.router, prefix="/configs", tags=["snat"])
|
||||
app.include_router(hosts.router, prefix="/configs", tags=["hosts"])
|
||||
app.include_router(params.router, prefix="/configs", tags=["params"])
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
@@ -35,6 +35,8 @@ class Config(Base):
|
||||
policies: Mapped[list["Policy"]] = relationship("Policy", back_populates="config", cascade="all, delete-orphan", order_by="Policy.position")
|
||||
rules: Mapped[list["Rule"]] = relationship("Rule", back_populates="config", cascade="all, delete-orphan", order_by="Rule.position")
|
||||
snat_entries: Mapped[list["Snat"]] = relationship("Snat", back_populates="config", cascade="all, delete-orphan")
|
||||
host_entries: Mapped[list["Host"]] = relationship("Host", back_populates="config", cascade="all, delete-orphan")
|
||||
params: Mapped[list["Param"]] = relationship("Param", back_populates="config", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Zone(Base):
|
||||
@@ -113,3 +115,28 @@ class Snat(Base):
|
||||
comment: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
config: Mapped["Config"] = relationship("Config", back_populates="snat_entries")
|
||||
|
||||
|
||||
class Host(Base):
|
||||
__tablename__ = "hosts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
config_id: Mapped[int] = mapped_column(Integer, ForeignKey("configs.id"), nullable=False)
|
||||
zone_id: Mapped[int] = mapped_column(Integer, ForeignKey("zones.id"), nullable=False)
|
||||
interface: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
subnet: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
options: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
config: Mapped["Config"] = relationship("Config", back_populates="host_entries")
|
||||
zone: Mapped["Zone"] = relationship("Zone")
|
||||
|
||||
|
||||
class Param(Base):
|
||||
__tablename__ = "params"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
config_id: Mapped[int] = mapped_column(Integer, ForeignKey("configs.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
value: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
|
||||
config: Mapped["Config"] = relationship("Config", back_populates="params")
|
||||
|
||||
@@ -186,6 +186,52 @@ class SnatOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# --- Host ---
|
||||
class HostCreate(BaseModel):
|
||||
zone_id: int
|
||||
interface: str
|
||||
subnet: str
|
||||
options: str = ""
|
||||
|
||||
|
||||
class HostUpdate(BaseModel):
|
||||
zone_id: Optional[int] = None
|
||||
interface: Optional[str] = None
|
||||
subnet: Optional[str] = None
|
||||
options: Optional[str] = None
|
||||
|
||||
|
||||
class HostOut(BaseModel):
|
||||
id: int
|
||||
config_id: int
|
||||
zone_id: int
|
||||
interface: str
|
||||
subnet: str
|
||||
options: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# --- Param ---
|
||||
class ParamCreate(BaseModel):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
|
||||
class ParamUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
value: Optional[str] = None
|
||||
|
||||
|
||||
class ParamOut(BaseModel):
|
||||
id: int
|
||||
config_id: int
|
||||
name: str
|
||||
value: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# --- Generate ---
|
||||
class GenerateOut(BaseModel):
|
||||
zones: str
|
||||
@@ -193,3 +239,5 @@ class GenerateOut(BaseModel):
|
||||
policy: str
|
||||
rules: str
|
||||
snat: str
|
||||
hosts: str
|
||||
params: str
|
||||
|
||||
@@ -41,6 +41,7 @@ class ShorewallGenerator:
|
||||
lines = [
|
||||
self._header("rules"),
|
||||
"#ACTION".ljust(16) + "SOURCE".ljust(24) + "DEST".ljust(24) + "PROTO".ljust(10) + "DPORT".ljust(10) + "SPORT\n",
|
||||
"SECTION NEW\n",
|
||||
]
|
||||
for r in sorted(self._config.rules, key=lambda x: x.position):
|
||||
src = (r.src_zone.name if r.src_zone else "all") + (f":{r.src_ip}" if r.src_ip else "")
|
||||
@@ -48,6 +49,19 @@ class ShorewallGenerator:
|
||||
lines.append(self._col(r.action, src, dst, r.proto or "-", r.dport or "-", r.sport or "-", width=16))
|
||||
return "".join(lines)
|
||||
|
||||
def hosts(self) -> str:
|
||||
lines = [self._header("hosts"), "#ZONE".ljust(16) + "HOSTS\n"]
|
||||
for h in self._config.host_entries:
|
||||
hosts_val = f"{h.interface}:{h.subnet}"
|
||||
lines.append(self._col(h.zone.name, hosts_val, h.options or "-", width=16))
|
||||
return "".join(lines)
|
||||
|
||||
def params(self) -> str:
|
||||
lines = [self._header("params")]
|
||||
for p in self._config.params:
|
||||
lines.append(f"{p.name}={p.value}\n")
|
||||
return "".join(lines)
|
||||
|
||||
def snat(self) -> str:
|
||||
lines = [self._header("snat"), "#ACTION".ljust(24) + "SOURCE".ljust(24) + "DEST\n"]
|
||||
for m in self._config.snat_entries:
|
||||
@@ -62,6 +76,8 @@ class ShorewallGenerator:
|
||||
"policy": self.policy(),
|
||||
"rules": self.rules(),
|
||||
"snat": self.snat(),
|
||||
"hosts": self.hosts(),
|
||||
"params": self.params(),
|
||||
}
|
||||
|
||||
def as_zip(self) -> bytes:
|
||||
@@ -72,4 +88,6 @@ class ShorewallGenerator:
|
||||
zf.writestr("policy", self.policy())
|
||||
zf.writestr("rules", self.rules())
|
||||
zf.writestr("snat", self.snat())
|
||||
zf.writestr("hosts", self.hosts())
|
||||
zf.writestr("params", self.params())
|
||||
return buf.getvalue()
|
||||
|
||||
Reference in New Issue
Block a user