Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/joaomonteir0/printheritage/llms.txt

Use this file to discover all available pages before exploring further.

Access control in PrintHeritage is enforced at two levels: a global role that travels with your user account and governs platform-wide operations, and a project-level permission that controls your access to specific projects. Both levels use the same GlobalRole enum, keeping the permission model uniform and easy to reason about. Every API request is validated against these roles before any data is read or written.

Global Roles

Every user account carries a single global_role value drawn from the GlobalRole enum defined in models.py. New accounts default to VISUALIZER unless a different role is specified at registration.
RoleDescription
SUPER_ADMINFull, unrestricted access to the entire platform. Bypasses all project-level permission checks. Can view audit logs, list all users, and perform any operation on any project.
GENERAL_ADMINAdministrative access without audit-log visibility. Bypasses project-level checks and can list all users platform-wide. Cannot access the /audit-logs endpoint.
PROJECT_ADMINProject-scoped administration. Must hold an ACCEPTED ProjectPermission for each project. Can add and delete datasets within permitted projects.
VISUALIZERRead-only access. Must hold an ACCEPTED ProjectPermission to view a project. Cannot delete datasets.
The global_role on a User record and the access_level on a ProjectPermission record are both typed as the same GlobalRole enum. When a user is added to a project, their current global_role is copied into access_level automatically, but the two values are stored independently and can diverge if a user’s global role is later updated.

Role Capabilities at a Glance

CapabilitySUPER_ADMINGENERAL_ADMINPROJECT_ADMINVISUALIZER
View audit logs
List all users (GET /users)
Bypass project-level access checks
View all projects platform-wide
Create projects
Read project data (with permission)
Add datasets to a project
Delete datasets from a project
Invite members to a project

The RoleChecker Guard

Endpoints that require a minimum global role use the RoleChecker dependency from security.py. It accepts a list of permitted roles. SUPER_ADMIN users always pass regardless of what roles are listed:
# Only SUPER_ADMIN and GENERAL_ADMIN may call this endpoint
@app.get("/users")
def list_users(
    _: bool = Depends(security.RoleChecker([
        models.GlobalRole.SUPER_ADMIN,
        models.GlobalRole.GENERAL_ADMIN
    ]))
):
    ...
RoleChecker checks the caller’s global role only. It does not evaluate project-level permissions. Use validate_project_access for project-scoped enforcement.

Project Permissions

The ProjectPermission model records a user’s membership in a specific project. Each row links one user to one project and cannot be duplicated (enforced by a UNIQUE constraint on (user_id, project_id)).
FieldTypeDescription
idUUIDAuto-generated identifier for the permission record.
user_idUUID (FK → users.id)The member being granted access.
project_idUUID (FK → projects.id)The project this permission applies to.
invited_byUUID (FK → users.id)The user who sent the invitation. Nullable for auto-created owner permissions.
access_levelGlobalRoleThe role this member holds within the project.
statusPermissionStatusInvitation lifecycle state: PENDING, ACCEPTED, or REJECTED.
is_readBooleanWhether the user has read the invitation notification.
is_favoriteBooleanWhether this user has marked the project as a favorite.

PermissionStatus

class PermissionStatus(str, enum.Enum):
    PENDING  = "PENDING"   # Invitation sent, awaiting response
    ACCEPTED = "ACCEPTED"  # User accepted; access is active
    REJECTED = "REJECTED"  # User declined the invitation
Only ACCEPTED permissions grant access to project data. PENDING invitations appear in the user’s invitation inbox at GET /invitations and can be acted on via POST /invitations/{id}/accept or POST /invitations/{id}/reject.

How validate_project_access Works

Every project-scoped endpoint calls security.validate_project_access before returning data. The check proceeds as follows:
  1. If the requesting user’s global_role is SUPER_ADMIN or GENERAL_ADMIN, access is granted immediately — no ProjectPermission row is required.
  2. Otherwise, the function queries for an ACCEPTED ProjectPermission matching the user and project. If none exists, a 403 Forbidden error is raised.
def validate_project_access(project_id: str, user: models.User, db: Session):
    if user.global_role in [models.GlobalRole.SUPER_ADMIN, models.GlobalRole.GENERAL_ADMIN]:
        return {"access_level": user.global_role, "granted": True}

    permission = db.query(models.ProjectPermission).filter(
        models.ProjectPermission.user_id == user.id,
        models.ProjectPermission.project_id == project_id,
        models.ProjectPermission.status == models.PermissionStatus.ACCEPTED
    ).first()

    if not permission:
        raise HTTPException(status_code=403, detail=f"...")

    return {"access_level": permission.access_level, "granted": True}
If you need to grant a colleague temporary read access to a project without promoting their global role, invite them to the specific project with access_level: VISUALIZER. Their global role on other projects remains unchanged.

Dataset Deletion Restriction

Deleting a dataset requires PROJECT_ADMIN or higher. VISUALIZER accounts receive HTTP 403 even when they hold an ACCEPTED project permission:
if current_user.global_role not in [
    models.GlobalRole.SUPER_ADMIN,
    models.GlobalRole.GENERAL_ADMIN,
    models.GlobalRole.PROJECT_ADMIN,
]:
    raise HTTPException(status_code=403, detail="Permissões insuficientes para apagar dados.")
This restriction exists independently of the project-level permission — a user must satisfy both checks: a valid ACCEPTED ProjectPermission and a sufficiently privileged global_role.

Build docs developers (and LLMs) love