Files
bambuddy/backend/app/models/group.py
T
maziggy fe07fbd07b Give groups whole locations and a printer access page (issue #1727)
A group can now be given locations as well as single printers: it then
reaches every printer in them, including printers added there later.
Printer access moves out of the group editor to Settings ->
Authentication -> Printer access, built for large fleets: groups and
printers searchable and filtered by location, model and access, edited
by group or by printer, saved together. "Who has access" in the printer
card menu opens it on that printer.

- group_locations table; the scope is picked printers plus printers
  whose location matches.
- Moving a printer into or out of a location a limited group has is
  admin-only, and open connections are rescoped. The edit dialog names
  the groups a move affects. Locations are trimmed on save.
- Clearing a printer's location in the edit dialog now clears it.
2026-10-02 07:30:04 +02:00

83 lines
3.3 KiB
Python

"""Group model for permission-based access control."""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Table, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON
from backend.app.core.database import Base
if TYPE_CHECKING:
from backend.app.models.user import User
# Many-to-many association table between users and groups
user_groups = Table(
"user_groups",
Base.metadata,
Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
Column("group_id", Integer, ForeignKey("groups.id", ondelete="CASCADE"), primary_key=True),
)
# Printers a group with ``restrict_printers`` set is allowed to see and control
# (#1727). Rows are only meaningful while the flag is on; turning it off keeps
# them so the selection survives toggling. SQLite doesn't enforce the FK
# cascades, so printer and group deletes remove their rows explicitly.
group_printers = Table(
"group_printers",
Base.metadata,
Column("group_id", Integer, ForeignKey("groups.id", ondelete="CASCADE"), primary_key=True),
Column("printer_id", Integer, ForeignKey("printers.id", ondelete="CASCADE"), primary_key=True),
)
# Locations a restricted group may use (#1727): every printer whose
# ``location`` matches, now or later, so a printer added to "Lab A" reaches
# the Lab A team without anyone ticking it. Matched exactly against
# ``printers.location``; a name no printer carries any more simply grants
# nothing. Like ``group_printers``, kept while the flag is off.
group_locations = Table(
"group_locations",
Base.metadata,
Column("group_id", Integer, ForeignKey("groups.id", ondelete="CASCADE"), primary_key=True),
Column("location", String(100), primary_key=True),
)
class Group(Base):
"""Group model for organizing users and assigning permissions.
Groups contain a list of permissions that are granted to all members.
Users can belong to multiple groups, and their permissions are additive.
System groups (Administrators, Operators, Viewers) cannot be deleted.
Permissions say *what* a member may do; ``restrict_printers`` says *where*.
A group without the flag doesn't narrow printer access at all, so it can be
combined with a team group that does. See ``core/printer_scope.py``.
"""
__tablename__ = "groups"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True, index=True)
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
permissions: Mapped[list[str]] = mapped_column(JSON, default=list)
is_system: Mapped[bool] = mapped_column(Boolean, default=False)
restrict_printers: Mapped[bool] = mapped_column(Boolean, default=False, server_default="0")
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationship to users through association table
users: Mapped[list[User]] = relationship(
"User",
secondary=user_groups,
back_populates="groups",
lazy="selectin",
)
def __repr__(self) -> str:
return f"<Group {self.name}>"