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

This commit is contained in:
2026-03-01 01:42:28 +01:00
parent 15f28cb070
commit 21d404229a
12 changed files with 308 additions and 4 deletions

View File

@@ -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
View 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
View 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()

View File

@@ -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")

View File

@@ -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")

View File

@@ -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

View File

@@ -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()