How RomM Implements User Roles and Permissions for Library Access Control
RomM controls library access through a layered permission system that combines a binary admin/user role flag with granular permission groups, per-user overrides, and hidden entity lists.
RomM, the self-hosted ROM and emulator management platform, implements a sophisticated authorization architecture that allows administrators to precisely control who can view, modify, or delete library content. According to the rommapp/romm source code, the system balances simple role-based access with fine-grained capability grants, making it suitable for both small personal collections and larger shared libraries.
The Two-Level Role Model
At the foundation of RomM's access control is a simple enum defined in backend/models/user.py (lines 25-31) that distinguishes between administrators and regular users:
class Role(enum.StrEnum):
USER = "user"
ADMIN = "admin"
Admins bypass every permission check automatically, granting them unrestricted access to all platforms, ROMs, and administrative functions. Users, conversely, must have their specific capabilities defined through permission groups and overrides. This binary distinction allows the system to short-circuit permission lookups for administrators while enforcing granular controls for everyone else.
Granular Permission Groups
For non-administrative users, RomM employs a PermissionGroup model defined in backend/models/permission.py (lines 62-88) that creates named permission sets:
class PermissionGroup(BaseModel):
name: Mapped[str] = mapped_column(String(255), unique=True, index=True)
grants: Mapped[list[PermissionGroupGrant]] = relationship(...)
Each PermissionGroupGrant specifies:
- Entity: The resource type (e.g.,
ROMS,PLATFORMS,COLLECTIONS) - Action: The operation allowed (
READ,WRITE,DELETE) - Ownership scope: Whether the grant applies only to resources the user owns (
own_only) or to all resources of that type
This design allows administrators to create reusable role templates such as "Editor" or "Viewer" and assign them to multiple users without configuring individual permissions for each account.
Per-User Permission Overrides
When a permission group does not provide exactly the right access level, RomM supports individual capability overrides through the UserPermissionOverride model in backend/models/permission.py (lines 116-136). These overrides modify the effective permissions calculated from the user's group assignment and take precedence over group grants.
As shown in the RomM implementation, overrides can grant additional capabilities not present in the user's group or revoke specific permissions that would otherwise be allowed. The model tracks the user ID, target entity, action type, and whether the exception applies only to owned resources.
Hidden Entities for Content Restriction
Beyond action-based permissions, RomM can restrict visibility of specific platforms or ROMs through the HiddenEntity model in backend/models/permission.py (lines 138-172). This feature allows administrators to hide individual resources from specific users or entire permission groups:
hidden = HiddenEntity(
entity=PermEntity.PLATFORMS,
entity_id=42, # platform PK
user_id=new_user.id, # hide only for this user
)
When user_id is set, the entity is hidden only for that specific user. When permission_group_id is set, the restriction applies to all members of that group. This mechanism enables sophisticated content filtering, such as hiding mature-rated platforms from certain user accounts or restricting access to specific ROM collections.
Permission Resolution Logic
The actual enforcement of permissions occurs in backend/handler/auth/permissions.py (lines 11-27) through the resolve_permissions() function. This function builds a ResolvedPermissions object that represents the user's effective capabilities:
- Admin short-circuit: If
user.role == Role.ADMIN, the function immediately returns full permissions without database lookups - Group resolution: For regular users, the function fetches the assigned permission group (or the server-wide default)
- Override merging: Individual user overrides are applied on top of group grants
- Hidden entity filtering: Lists of restricted platforms and ROMs are attached to the result
The resolved permissions object provides helper methods like can_see_rom(rom_id, platform_id) that endpoints use to validate requests before processing.
Legacy OAuth Scope Integration
RomM maintains backward compatibility with an older scope-based authorization system. The compute_oauth_scopes() function in backend/handler/auth/permissions.py (lines 65-84) projects granular permission grants onto legacy OAuth scopes (such as Scope.ME_READ or Scope.ROMS_WRITE).
The mapping logic in backend/handler/auth/permissions_map.py (lines 1-24) translates specific entity-action combinations into coarse OAuth scopes:
def grants_to_scopes(grants: list[PermissionGroupGrant]) -> list[str]:
scopes = set()
for grant in grants:
if grant.entity == PermEntity.ROMS and grant.action == PermAction.WRITE:
scopes.add("roms.write")
return list(scopes)
This translation layer ensures that API clients using legacy authentication tokens receive appropriate access levels while the internal system moves toward the more granular permission model.
Kiosk Mode Restrictions
When RomM operates in kiosk mode, the permission system enforces additional restrictions. The KIOSK_MODE configuration flag forces all non-admin users to read-only access regardless of their permission group assignments. This override occurs in both the resolve_permissions() function and the OAuth scope calculation (_compute_non_admin_scopes()), ensuring that kiosk users cannot modify library content even if their user account otherwise has write permissions.
Practical Implementation Examples
Creating a user with custom permissions involves several steps through RomM's database handlers:
# Create a regular user with default role
new_user = User(
username="alice",
hashed_password="...",
role=Role.USER,
enabled=True,
)
db_user_handler.create_user(new_user)
# Assign to a permission group
editor_group = db_permission_handler.get_group_by_name("Editor")
db_user_handler.update_user(new_user.id, {"permission_group_id": editor_group.id})
# Add a specific override for ROM write access
override = UserPermissionOverride(
user_id=new_user.id,
entity=PermEntity.ROMS,
action=PermAction.WRITE,
granted=True,
own_only=False,
)
db_permission_handler.create_user_override(override)
# Hide a specific platform from the user
hidden = HiddenEntity(
entity=PermEntity.PLATFORMS,
entity_id=42,
user_id=new_user.id,
)
db_permission_handler.create_hidden_entity(hidden)
In API endpoints, permissions are checked using the dependency injection pattern:
from backend.handler.auth.permissions import resolve_permissions
@app.get("/roms/{rom_id}")
def get_rom(rom_id: int, user: User = Depends(get_current_user)):
perms = resolve_permissions(user)
if not perms.can_see_rom(rom_id, platform_id):
raise HTTPException(status_code=404, detail="Not found")
return db_rom_handler.get_rom(rom_id)
Summary
- Binary role foundation: RomM uses a simple
admin/userenum inbackend/models/user.pyto provide immediate full access or trigger granular permission checks - Permission groups: Named collections of entity-action grants in
backend/models/permission.pyserve as reusable templates for user capabilities - Per-user overrides: The
UserPermissionOverridemodel allows fine-tuning of group permissions for individual accounts - Content hiding: The
HiddenEntitymodel restricts visibility of specific platforms or ROMs per-user or per-group - Resolution logic: The
resolve_permissions()function inbackend/handler/auth/permissions.pycalculates effective permissions by merging groups, overrides, and hidden entities - Legacy support: OAuth scopes are computed from granular grants via
backend/handler/auth/permissions_map.pyfor backward compatibility - Kiosk mode: When enabled, forces read-only access for all non-admin users regardless of assigned permissions
Frequently Asked Questions
How do I grant a user full access to all ROMs without making them an admin?
Assign the user to a permission group that includes a grant with entity=PermEntity.ROMS, action=PermAction.WRITE, and own_only=False. As implemented in backend/models/permission.py, setting own_only=False grants access to all ROMs of that entity type rather than just those owned by the user. If no existing group provides these permissions, create a new permission group and assign the user to it.
What is the difference between permission groups and user overrides?
Permission groups are reusable templates defined in backend/models/permission.py that assign the same set of capabilities to multiple users. User overrides are specific to individual accounts and modify the effective permissions calculated from the user's group. Overrides can add capabilities not present in the group or revoke specific permissions that the group would otherwise allow. The resolver in backend/handler/auth/permissions.py applies group grants first, then processes overrides to determine the final permission set.
How does kiosk mode affect existing user permissions?
When KIOSK_MODE is enabled, the permission resolution logic in backend/handler/auth/permissions.py forces all non-admin users to read-only access regardless of their permission group assignments or individual overrides. This affects both the internal ResolvedPermissions object and the OAuth scopes returned by compute_oauth_scopes(). Users retain their group memberships and override settings in the database, but the system ignores write and delete permissions while kiosk mode remains active.
Can I hide specific platforms from certain users while allowing access to others?
Yes. Create a HiddenEntity record in backend/models/permission.py specifying the platform ID and either the user_id to hide it from a specific user or the permission_group_id to hide it from an entire group. The permission resolver checks these hidden entity lists during resolve_permissions() and filters out restricted platforms from search results and API responses. This operates independently from action-based permissions, meaning a user could have READ permission for platforms generally but still be blocked from seeing a specific hidden platform.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →