mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 12:46:49 +02:00
Users can authenticate against an LDAP/AD server with configurable server URL, bind DN, search base, and user filter. Supports StartTLS and LDAPS — plaintext is not allowed. Both Active Directory (memberOf) and POSIX groups (memberUid) are mapped to BamBuddy groups on each login. Auto-provisioning creates local accounts on first LDAP login. Local admin accounts remain as fallback when LDAP is unreachable. Password management is disabled for LDAP users.
116 lines
4.2 KiB
Python
116 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy import DateTime, String, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
if TYPE_CHECKING:
|
|
from backend.app.models.group import Group
|
|
from backend.app.models.user_email_pref import UserEmailPreference
|
|
|
|
|
|
class User(Base):
|
|
"""User model for authentication and authorization.
|
|
|
|
Users can belong to multiple groups, and their permissions are additive
|
|
across all groups. The legacy 'role' field is kept for backward compatibility
|
|
but is_admin property now also considers group membership.
|
|
"""
|
|
|
|
__tablename__ = "users"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
|
email: Mapped[str | None] = mapped_column(String(255), unique=True, index=True, nullable=True)
|
|
password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
role: Mapped[str] = mapped_column(
|
|
String(20), default="user"
|
|
) # "admin" or "user" (legacy, kept for backward compat)
|
|
auth_source: Mapped[str] = mapped_column(String(20), default="local") # "local" or "ldap"
|
|
is_active: Mapped[bool] = mapped_column(default=True)
|
|
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())
|
|
|
|
# Per-user Bambu Cloud credentials (when auth is enabled, each user has their own)
|
|
cloud_token: Mapped[str | None] = mapped_column(String(500), nullable=True, default=None)
|
|
cloud_email: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
|
|
|
# Relationship to groups through association table
|
|
groups: Mapped[list[Group]] = relationship(
|
|
"Group",
|
|
secondary="user_groups",
|
|
back_populates="users",
|
|
lazy="selectin",
|
|
)
|
|
|
|
# Relationship to email notification preferences
|
|
email_preferences: Mapped[UserEmailPreference | None] = relationship(
|
|
"UserEmailPreference",
|
|
back_populates="user",
|
|
uselist=False,
|
|
cascade="all, delete-orphan",
|
|
lazy="select",
|
|
)
|
|
|
|
@property
|
|
def is_admin(self) -> bool:
|
|
"""Check if user is an admin.
|
|
|
|
Returns True if:
|
|
- User has legacy role='admin', OR
|
|
- User belongs to the Administrators group
|
|
"""
|
|
if self.role == "admin":
|
|
return True
|
|
return any(g.name == "Administrators" for g in self.groups)
|
|
|
|
def get_permissions(self) -> set[str]:
|
|
"""Get all permissions from all groups the user belongs to.
|
|
|
|
Returns a set of permission strings. Permissions are additive across groups.
|
|
"""
|
|
permissions: set[str] = set()
|
|
for group in self.groups:
|
|
if group.permissions:
|
|
permissions.update(group.permissions)
|
|
return permissions
|
|
|
|
def has_permission(self, permission: str) -> bool:
|
|
"""Check if user has a specific permission.
|
|
|
|
Admins have all permissions. For other users, checks if the permission
|
|
exists in any of their groups.
|
|
"""
|
|
if self.is_admin:
|
|
return True
|
|
return permission in self.get_permissions()
|
|
|
|
def has_all_permissions(self, *permissions: str) -> bool:
|
|
"""Check if user has ALL specified permissions.
|
|
|
|
Admins have all permissions. For other users, checks if all permissions
|
|
exist in their combined group permissions.
|
|
"""
|
|
if self.is_admin:
|
|
return True
|
|
user_permissions = self.get_permissions()
|
|
return all(p in user_permissions for p in permissions)
|
|
|
|
def has_any_permission(self, *permissions: str) -> bool:
|
|
"""Check if user has ANY of the specified permissions.
|
|
|
|
Admins have all permissions. For other users, checks if at least one
|
|
permission exists in their combined group permissions.
|
|
"""
|
|
if self.is_admin:
|
|
return True
|
|
user_permissions = self.get_permissions()
|
|
return any(p in user_permissions for p in permissions)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<User {self.username}>"
|