Add pilots: invite-only products with per-user RDP slots
New Service.is_pilot flag - a pilot is a Service (type RDP) that: - is excluded from /api/public/services-by-category and /api/request-more-access (admin-granted only, no self-service) - still shows as a locked card on the dashboard for users without access (Доступно по запросу section), but with a По приглашению badge instead of the self-request button/flow - gets its own non-clickable teaser row on the public /login page (logos only, informational) RdpSlot.assigned_user_id (nullable) - pilot slots are bound to one specific user instead of being drawn from the shared pool; regular RDP services are unaffected (field stays NULL, same pool behaviour as before). The /go/ allocator branches on service.is_pilot to pick the caller's assigned slot instead of any free one. Slots release automatically (cleanup_loop) when the owning grant is revoked or expires, and immediately on manual ACL revoke. UserServiceAccess.expires_at (nullable) - per-grant access window, used by pilots so their access can be shorter than the account's own expires_at; NULL (unchanged default) means "follow the account". has_access() and the dashboard's granted/locked split both honour it. Admin UI: "Это пилот" checkbox on the RDP service form, an assign-user dropdown on a pilot's slot table (replaces the occupied-by column), and a per-pilot expiry date field in the user ACL grid. Schema is applied via the existing ensure_schema_compatibility() idempotent ALTER TABLE pattern (no alembic in this project) - no manual migration step needed, it runs at container startup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,12 @@ class Service(Base):
|
||||
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
||||
# Pilot = invite-only product. Hidden from the public/request-more-access
|
||||
# catalogs (admin grants access by hand instead of self-service), and its
|
||||
# RdpSlot rows are bound to specific users (RdpSlot.assigned_user_id)
|
||||
# rather than drawn from a shared pool. See _apply_access_decision /
|
||||
# pilot slot allocation in main.py for where this flag is read.
|
||||
is_pilot: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||
|
||||
|
||||
class Category(Base):
|
||||
@@ -83,6 +89,11 @@ class UserServiceAccess(Base):
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
|
||||
service_id: Mapped[int] = mapped_column(ForeignKey("services.id", ondelete="CASCADE"), index=True)
|
||||
granted_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
||||
# Per-grant expiry, used by pilots so a pilot's access window can be
|
||||
# shorter than the user's overall account expires_at. NULL (the default,
|
||||
# and the only value regular non-pilot grants ever get) means "follow
|
||||
# the account's own expires_at" - see has_access() in auth.py.
|
||||
expires_at: Mapped[Optional[dt.datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class RdpSlot(Base):
|
||||
@@ -94,6 +105,11 @@ class RdpSlot(Base):
|
||||
rdp_password: Mapped[str] = mapped_column(String(256), default="")
|
||||
container_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True)
|
||||
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
||||
# Pilot slots are reserved for one specific person instead of being
|
||||
# drawn from a shared pool - set only on slots that belong to a
|
||||
# Service with is_pilot=True. NULL means "ordinary pooled slot",
|
||||
# unchanged behaviour for every existing RDP service.
|
||||
assigned_user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
|
||||
|
||||
class SessionModel(Base):
|
||||
|
||||
Reference in New Issue
Block a user